Comic Rework (Part 1) (#2772)
This commit is contained in:
parent
58c77b32b1
commit
fc21073898
69 changed files with 5090 additions and 703 deletions
|
@ -19,9 +19,8 @@ public class ParserInfoListExtensions
|
|||
private readonly IDefaultParser _defaultParser;
|
||||
public ParserInfoListExtensions()
|
||||
{
|
||||
_defaultParser =
|
||||
new DefaultParser(new DirectoryService(Substitute.For<ILogger<DirectoryService>>(),
|
||||
new MockFileSystem()));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
|
||||
_defaultParser = new BasicParser(ds, new ImageParser(ds));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -43,7 +42,7 @@ public class ParserInfoListExtensions
|
|||
{
|
||||
infos.Add(_defaultParser.Parse(
|
||||
Path.Join("E:/Manga/Cynthia the Mission/", filename),
|
||||
"E:/Manga/"));
|
||||
"E:/Manga/", "E:/Manga/", LibraryType.Manga));
|
||||
}
|
||||
|
||||
var files = inputChapters.Select(s => new MangaFileBuilder(s, MangaFormat.Archive, 199).Build()).ToList();
|
||||
|
@ -61,7 +60,7 @@ public class ParserInfoListExtensions
|
|||
{
|
||||
_defaultParser.Parse(
|
||||
"E:/Manga/Cynthia the Mission/Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip",
|
||||
"E:/Manga/")
|
||||
"E:/Manga/", "E:/Manga/", LibraryType.Manga)
|
||||
};
|
||||
|
||||
var files = new[] {@"E:\Manga\Cynthia the Mission\Cynthia The Mission The Special SP01 [Desudesu&Brolen].zip"}
|
||||
|
|
217
API.Tests/Parsers/BasicParserTests.cs
Normal file
217
API.Tests/Parsers/BasicParserTests.cs
Normal file
|
@ -0,0 +1,217 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parsers;
|
||||
|
||||
public class BasicParserTests
|
||||
{
|
||||
private readonly BasicParser _parser;
|
||||
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private const string RootDirectory = "C:/Books/";
|
||||
|
||||
public BasicParserTests()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddDirectory("C:/Books/");
|
||||
fileSystem.AddFile("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", new MockFileData(""));
|
||||
|
||||
fileSystem.AddFile("C:/Books/Accel World/Accel World - Volume 1.cbz", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Books/Accel World/Accel World - Volume 1 Chapter 2.cbz", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Books/Accel World/Accel World - Chapter 3.cbz", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Books/Accel World/Accel World Gaiden SP01.cbz", new MockFileData(""));
|
||||
|
||||
|
||||
fileSystem.AddFile("C:/Books/Accel World/cover.png", new MockFileData(""));
|
||||
|
||||
fileSystem.AddFile("C:/Books/Batman/Batman #1.cbz", new MockFileData(""));
|
||||
|
||||
var ds = new DirectoryService(_dsLogger, fileSystem);
|
||||
_parser = new BasicParser(ds, new ImageParser(ds));
|
||||
}
|
||||
|
||||
#region Parse_Books
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parse_Manga
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is a loose leaf cover in the manga library, that it is ignored
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
|
||||
{
|
||||
var actual = _parser.Parse(@"C:/Books/Accel World/cover.png", "C:/Books/Accel World/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is a loose leaf cover in the manga library, that it is ignored
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_OtherImage_ShouldReturnNull()
|
||||
{
|
||||
var actual = _parser.Parse(@"C:/Books/Accel World/page 01.png", "C:/Books/Accel World/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is a volume and chapter in filename, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_VolumeAndChapterInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz", "C:/Books/Mujaki no Rakuen/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Mujaki no Rakuen", actual.Series);
|
||||
Assert.Equal("12", actual.Volumes);
|
||||
Assert.Equal("76", actual.Chapters);
|
||||
Assert.False(actual.IsSpecial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is a volume in filename, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_JustVolumeInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz",
|
||||
"C:/Books/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", actual.Series);
|
||||
Assert.Equal("1", actual.Volumes);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
Assert.False(actual.IsSpecial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is a chapter only in filename, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_JustChapterInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Beelzebub/Beelzebub_01_[Noodles].zip",
|
||||
"C:/Books/Beelzebub/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Beelzebub", actual.Series);
|
||||
Assert.Equal(Parser.LooseLeafVolume, actual.Volumes);
|
||||
Assert.Equal("1", actual.Chapters);
|
||||
Assert.False(actual.IsSpecial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is a SP Marker in filename, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_SpecialMarkerInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr",
|
||||
"C:/Books/Summer Time Rendering/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Summer Time Rendering", actual.Series);
|
||||
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
Assert.True(actual.IsSpecial);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when the filename parses as a speical, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_SpecialInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Summer Time Rendering/Specials/Volume Omake.cbr",
|
||||
"C:/Books/Summer Time Rendering/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Summer Time Rendering", actual.Series);
|
||||
Assert.Equal("Volume Omake", actual.Title);
|
||||
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
Assert.True(actual.IsSpecial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when there is an edition in filename, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_EditionInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Air Gear/Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz",
|
||||
"C:/Books/Air Gear/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Air Gear", actual.Series);
|
||||
Assert.Equal("1", actual.Volumes);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
Assert.False(actual.IsSpecial);
|
||||
Assert.Equal("Omnibus", actual.Edition);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parse_Books
|
||||
/// <summary>
|
||||
/// Tests that when there is a volume in filename, it appropriately parses
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_MangaBooks_JustVolumeInFilename()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/Epubs/Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub",
|
||||
"C:/Books/Epubs/",
|
||||
RootDirectory, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
|
||||
Assert.Equal("Harrison, Kim - The Good, The Bad, and the Undead - Hollows", actual.Series);
|
||||
Assert.Equal("2.5", actual.Volumes);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsApplicable
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on images and Image library type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
|
||||
{
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image));
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.ComicVine));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on images and Image library type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Success_WhenMatchingLibraryType()
|
||||
{
|
||||
Assert.True(_parser.IsApplicable("something.png", LibraryType.Manga));
|
||||
Assert.True(_parser.IsApplicable("something.png", LibraryType.Comic));
|
||||
Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Book));
|
||||
Assert.True(_parser.IsApplicable("something.epub", LibraryType.LightNovel));
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
74
API.Tests/Parsers/BookParserTests.cs
Normal file
74
API.Tests/Parsers/BookParserTests.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parsers;
|
||||
|
||||
public class BookParserTests
|
||||
{
|
||||
private readonly BookParser _parser;
|
||||
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private const string RootDirectory = "C:/Books/";
|
||||
|
||||
public BookParserTests()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddDirectory("C:/Books/");
|
||||
fileSystem.AddFile("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Books/Adam Freeman - Pro ASP.NET Core 6.epub", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Books/My Fav Book SP01.epub", new MockFileData(""));
|
||||
var ds = new DirectoryService(_dsLogger, fileSystem);
|
||||
_parser = new BookParser(ds, Substitute.For<IBookService>(), new BasicParser(ds, new ImageParser(ds)));
|
||||
}
|
||||
|
||||
#region Parse
|
||||
|
||||
// TODO: I'm not sure how to actually test this as it relies on an epub parser to actually do anything
|
||||
|
||||
/// <summary>
|
||||
/// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter
|
||||
/// </summary>
|
||||
// [Fact]
|
||||
// public void Parse_SeriesWithDirectoryName()
|
||||
// {
|
||||
// var actual = _parser.Parse("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", "C:/Books/Birds of Prey/",
|
||||
// RootDirectory, LibraryType.Book, new ComicInfo()
|
||||
// {
|
||||
// Series = "Harry Potter",
|
||||
// Volume = "1"
|
||||
// });
|
||||
//
|
||||
// Assert.NotNull(actual);
|
||||
// Assert.Equal("Harry Potter", actual.Series);
|
||||
// Assert.Equal("1", actual.Volumes);
|
||||
// }
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsApplicable
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on images and Image library type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
|
||||
{
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga));
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Book));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on images and Image library type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Success_WhenMatchingLibraryType()
|
||||
{
|
||||
Assert.True(_parser.IsApplicable("something.epub", LibraryType.Image));
|
||||
}
|
||||
#endregion
|
||||
}
|
115
API.Tests/Parsers/ComicVineParserTests.cs
Normal file
115
API.Tests/Parsers/ComicVineParserTests.cs
Normal file
|
@ -0,0 +1,115 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parsers;
|
||||
|
||||
public class ComicVineParserTests
|
||||
{
|
||||
private readonly ComicVineParser _parser;
|
||||
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private const string RootDirectory = "C:/Comics/";
|
||||
|
||||
public ComicVineParserTests()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddDirectory("C:/Comics/");
|
||||
fileSystem.AddDirectory("C:/Comics/Birds of Prey (2002)");
|
||||
fileSystem.AddFile("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", new MockFileData(""));
|
||||
var ds = new DirectoryService(_dsLogger, fileSystem);
|
||||
_parser = new ComicVineParser(ds);
|
||||
}
|
||||
|
||||
#region Parse
|
||||
|
||||
/// <summary>
|
||||
/// Tests that when Series and Volume are filled out, Kavita uses that for the Series Name
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_SeriesWithComicInfo()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/",
|
||||
RootDirectory, LibraryType.ComicVine, new ComicInfo()
|
||||
{
|
||||
Series = "Birds of Prey",
|
||||
Volume = "2002"
|
||||
});
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Birds of Prey (2002)", actual.Series);
|
||||
Assert.Equal("2002", actual.Volumes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that no ComicInfo, take the Directory Name if it matches "Series (2002)" or "Series (2)"
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_SeriesWithDirectoryNameAsSeriesYear()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/Birds of Prey (2002)/Birds of Prey 001 (2002).cbz", "C:/Comics/Birds of Prey (2002)/",
|
||||
RootDirectory, LibraryType.ComicVine, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Birds of Prey (2002)", actual.Series);
|
||||
Assert.Equal("2002", actual.Volumes);
|
||||
Assert.Equal("1", actual.Chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that no ComicInfo, take a directory name up to root if it matches "Series (2002)" or "Series (2)"
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_SeriesWithADirectoryNameAsSeriesYear()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/DC Comics/Birds of Prey (1999)/Birds of Prey 001 (1999).cbz", "C:/Comics/DC Comics/",
|
||||
RootDirectory, LibraryType.ComicVine, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Birds of Prey (1999)", actual.Series);
|
||||
Assert.Equal("1999", actual.Volumes);
|
||||
Assert.Equal("1", actual.Chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that no ComicInfo and nothing matches Series (Volume), then just take the directory name as the Series
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_FallbackToDirectoryNameOnly()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/",
|
||||
RootDirectory, LibraryType.ComicVine, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Blood Syndicate", actual.Series);
|
||||
Assert.Equal(Parser.LooseLeafVolume, actual.Volumes);
|
||||
Assert.Equal("1", actual.Chapters);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IsApplicable
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on ComicVine type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
|
||||
{
|
||||
Assert.False(_parser.IsApplicable("", LibraryType.Comic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on ComicVine type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Success_WhenMatchingLibraryType()
|
||||
{
|
||||
Assert.True(_parser.IsApplicable("", LibraryType.ComicVine));
|
||||
}
|
||||
#endregion
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Linq;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
@ -9,9 +7,8 @@ using Microsoft.Extensions.Logging;
|
|||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
namespace API.Tests.Parsers;
|
||||
|
||||
public class DefaultParserTests
|
||||
{
|
||||
|
@ -22,10 +19,12 @@ public class DefaultParserTests
|
|||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
|
||||
_defaultParser = new DefaultParser(directoryService);
|
||||
_defaultParser = new BasicParser(directoryService, new ImageParser(directoryService));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#region ParseFromFallbackFolders
|
||||
[Theory]
|
||||
[InlineData("C:/", "C:/Love Hina/Love Hina - Special.cbz", "Love Hina")]
|
||||
|
@ -34,7 +33,7 @@ public class DefaultParserTests
|
|||
[InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")]
|
||||
public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries)
|
||||
{
|
||||
var actual = _defaultParser.Parse(inputPath, rootDir);
|
||||
var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, null);
|
||||
if (actual == null)
|
||||
{
|
||||
Assert.NotNull(actual);
|
||||
|
@ -52,7 +51,7 @@ public class DefaultParserTests
|
|||
public void ParseFromFallbackFolders_ShouldParseSeriesVolumeAndChapter(string inputFile, string[] expectedParseInfo)
|
||||
{
|
||||
const string rootDirectory = "/manga/";
|
||||
var actual = new ParserInfo {Series = "", Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume};
|
||||
var actual = new ParserInfo {Series = "", Chapters = Parser.DefaultChapter, Volumes = Parser.LooseLeafVolume};
|
||||
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
|
||||
Assert.Equal(expectedParseInfo[0], actual.Series);
|
||||
Assert.Equal(expectedParseInfo[1], actual.Volumes);
|
||||
|
@ -74,8 +73,8 @@ public class DefaultParserTests
|
|||
fs.AddDirectory(rootDirectory);
|
||||
fs.AddFile(inputFile, new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
|
||||
var parser = new DefaultParser(ds);
|
||||
var actual = parser.Parse(inputFile, rootDirectory);
|
||||
var parser = new BasicParser(ds, new ImageParser(ds));
|
||||
var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null);
|
||||
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
|
||||
Assert.Equal(expectedParseInfo, actual.Series);
|
||||
}
|
||||
|
@ -90,8 +89,8 @@ public class DefaultParserTests
|
|||
fs.AddDirectory(rootDirectory);
|
||||
fs.AddFile(inputFile, new MockFileData(""));
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
|
||||
var parser = new DefaultParser(ds);
|
||||
var actual = parser.Parse(inputFile, rootDirectory);
|
||||
var parser = new BasicParser(ds, new ImageParser(ds));
|
||||
var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null);
|
||||
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
|
||||
Assert.Equal(expectedParseInfo, actual.Series);
|
||||
}
|
||||
|
@ -101,13 +100,6 @@ public class DefaultParserTests
|
|||
|
||||
#region Parse
|
||||
|
||||
[Fact]
|
||||
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
|
||||
{
|
||||
const string rootPath = @"E:/Manga/";
|
||||
var actual = _defaultParser.Parse(@"E:/Manga/Accel World/cover.png", rootPath);
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_ParseInfo_Manga()
|
||||
|
@ -134,11 +126,12 @@ public class DefaultParserTests
|
|||
filepath = @"E:\Manga\Beelzebub\Beelzebub_01_[Noodles].zip";
|
||||
expected.Add(filepath, new ParserInfo
|
||||
{
|
||||
Series = "Beelzebub", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume,
|
||||
Series = "Beelzebub", Volumes = Parser.LooseLeafVolume,
|
||||
Chapters = "1", Filename = "Beelzebub_01_[Noodles].zip", Format = MangaFormat.Archive,
|
||||
FullFilePath = filepath
|
||||
});
|
||||
|
||||
// Note: Lots of duplicates here. I think I can move them to the ParserTests itself
|
||||
filepath = @"E:\Manga\Ichinensei ni Nacchattara\Ichinensei_ni_Nacchattara_v01_ch01_[Taruby]_v1.1.zip";
|
||||
expected.Add(filepath, new ParserInfo
|
||||
{
|
||||
|
@ -258,7 +251,7 @@ public class DefaultParserTests
|
|||
foreach (var file in expected.Keys)
|
||||
{
|
||||
var expectedInfo = expected[file];
|
||||
var actual = _defaultParser.Parse(file, rootPath);
|
||||
var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, null);
|
||||
if (expectedInfo == null)
|
||||
{
|
||||
Assert.Null(actual);
|
||||
|
@ -283,7 +276,7 @@ public class DefaultParserTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
//[Fact]
|
||||
public void Parse_ParseInfo_Manga_ImageOnly()
|
||||
{
|
||||
// Images don't have root path as E:\Manga, but rather as the path of the folder
|
||||
|
@ -296,7 +289,7 @@ public class DefaultParserTests
|
|||
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
};
|
||||
var actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Monster #8");
|
||||
var actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Manga, null);
|
||||
Assert.NotNull(actual2);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expectedInfo2.Format, actual2.Format);
|
||||
|
@ -322,7 +315,7 @@ public class DefaultParserTests
|
|||
FullFilePath = filepath, IsSpecial = false
|
||||
};
|
||||
|
||||
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\");
|
||||
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga",LibraryType.Manga, null);
|
||||
Assert.NotNull(actual2);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expectedInfo2.Format, actual2.Format);
|
||||
|
@ -348,7 +341,7 @@ public class DefaultParserTests
|
|||
FullFilePath = filepath, IsSpecial = false
|
||||
};
|
||||
|
||||
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\");
|
||||
actual2 = _defaultParser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Manga, null);
|
||||
Assert.NotNull(actual2);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expectedInfo2.Format, actual2.Format);
|
||||
|
@ -379,7 +372,7 @@ public class DefaultParserTests
|
|||
filesystem.AddFile(@"E:/Manga/Foo 50/Specials/Foo 50 SP01.cbz", new MockFileData(""));
|
||||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var parser = new DefaultParser(ds);
|
||||
var parser = new BasicParser(ds, new ImageParser(ds));
|
||||
|
||||
var filepath = @"E:/Manga/Foo 50/Foo 50 v1.cbz";
|
||||
// There is a bad parse for series like "Foo 50", so we have parsed chapter as 50
|
||||
|
@ -390,7 +383,7 @@ public class DefaultParserTests
|
|||
FullFilePath = filepath
|
||||
};
|
||||
|
||||
var actual = parser.Parse(filepath, rootPath);
|
||||
var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
|
@ -419,7 +412,7 @@ public class DefaultParserTests
|
|||
FullFilePath = filepath
|
||||
};
|
||||
|
||||
actual = parser.Parse(filepath, rootPath);
|
||||
actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null);
|
||||
Assert.NotNull(actual);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expected.Format, actual.Format);
|
||||
|
@ -444,7 +437,7 @@ public class DefaultParserTests
|
|||
[Fact]
|
||||
public void Parse_ParseInfo_Comic()
|
||||
{
|
||||
const string rootPath = @"E:/Comics/";
|
||||
const string rootPath = "E:/Comics/";
|
||||
var expected = new Dictionary<string, ParserInfo>();
|
||||
var filepath = @"E:/Comics/Teen Titans/Teen Titans v1 Annual 01 (1967) SP01.cbr";
|
||||
expected.Add(filepath, new ParserInfo
|
||||
|
@ -482,7 +475,7 @@ public class DefaultParserTests
|
|||
foreach (var file in expected.Keys)
|
||||
{
|
||||
var expectedInfo = expected[file];
|
||||
var actual = _defaultParser.Parse(file, rootPath, LibraryType.Comic);
|
||||
var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, null);
|
||||
if (expectedInfo == null)
|
||||
{
|
||||
Assert.Null(actual);
|
97
API.Tests/Parsers/ImageParserTests.cs
Normal file
97
API.Tests/Parsers/ImageParserTests.cs
Normal file
|
@ -0,0 +1,97 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parsers;
|
||||
|
||||
public class ImageParserTests
|
||||
{
|
||||
private readonly ImageParser _parser;
|
||||
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private const string RootDirectory = "C:/Comics/";
|
||||
|
||||
public ImageParserTests()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddDirectory("C:/Comics/");
|
||||
fileSystem.AddDirectory("C:/Comics/Birds of Prey (2002)");
|
||||
fileSystem.AddFile("C:/Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData(""));
|
||||
var ds = new DirectoryService(_dsLogger, fileSystem);
|
||||
_parser = new ImageParser(ds);
|
||||
}
|
||||
|
||||
#region Parse
|
||||
|
||||
/// <summary>
|
||||
/// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_SeriesWithDirectoryName()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/",
|
||||
RootDirectory, LibraryType.Image, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Birds of Prey", actual.Series);
|
||||
Assert.Equal("1", actual.Chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that if there is a Series Folder only, the code appropriately identifies the Series name from folder
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_SeriesWithNoNestedChapter()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/",
|
||||
RootDirectory, LibraryType.Image, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Birds of Prey", actual.Series);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that if there is a Series Folder only, the code appropriately identifies the Series name from folder and everything else as a
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_SeriesWithLooseImages()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/",
|
||||
RootDirectory, LibraryType.Image, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("Birds of Prey", actual.Series);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
Assert.True(actual.IsSpecial);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsApplicable
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on images and Image library type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
|
||||
{
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga));
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image));
|
||||
Assert.False(_parser.IsApplicable("something.epub", LibraryType.Image));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on images and Image library type
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Success_WhenMatchingLibraryType()
|
||||
{
|
||||
Assert.True(_parser.IsApplicable("something.png", LibraryType.Image));
|
||||
}
|
||||
#endregion
|
||||
}
|
71
API.Tests/Parsers/PdfParserTests.cs
Normal file
71
API.Tests/Parsers/PdfParserTests.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parsers;
|
||||
|
||||
public class PdfParserTests
|
||||
{
|
||||
private readonly PdfParser _parser;
|
||||
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||
private const string RootDirectory = "C:/Books/";
|
||||
|
||||
public PdfParserTests()
|
||||
{
|
||||
var fileSystem = new MockFileSystem();
|
||||
fileSystem.AddDirectory("C:/Books/");
|
||||
fileSystem.AddDirectory("C:/Books/Birds of Prey (2002)");
|
||||
fileSystem.AddFile("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", new MockFileData(""));
|
||||
fileSystem.AddFile("C:/Comics/DC Comics/Birds of Prey/Chapter 01/01.jpg", new MockFileData(""));
|
||||
var ds = new DirectoryService(_dsLogger, fileSystem);
|
||||
_parser = new PdfParser(ds);
|
||||
}
|
||||
|
||||
#region Parse
|
||||
|
||||
/// <summary>
|
||||
/// Tests that if there is a Series Folder then Chapter folder, the code appropriately identifies the Series name and Chapter
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Parse_Book_SeriesWithDirectoryName()
|
||||
{
|
||||
var actual = _parser.Parse("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf",
|
||||
"C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/",
|
||||
RootDirectory, LibraryType.Book, null);
|
||||
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series);
|
||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||
Assert.True(actual.IsSpecial);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsApplicable
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on pdfs
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Fails_WhenNonMatchingLibraryType()
|
||||
{
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Manga));
|
||||
Assert.False(_parser.IsApplicable("something.cbz", LibraryType.Image));
|
||||
Assert.False(_parser.IsApplicable("something.epub", LibraryType.Image));
|
||||
Assert.False(_parser.IsApplicable("something.png", LibraryType.Book));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that this Parser can only be used on pdfs
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsApplicable_Success_WhenMatchingLibraryType()
|
||||
{
|
||||
Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Book));
|
||||
Assert.True(_parser.IsApplicable("something.pdf", LibraryType.Manga));
|
||||
}
|
||||
#endregion
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
namespace API.Tests.Parsing;
|
||||
|
||||
public class BookParserTests
|
||||
public class BookParsingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Gifting The Wonderful World With Blessings! - 3 Side Stories [yuNS][Unknown]", "Gifting The Wonderful World With Blessings!")]
|
|
@ -6,20 +6,18 @@ using NSubstitute;
|
|||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
namespace API.Tests.Parsing;
|
||||
|
||||
public class ComicParserTests
|
||||
public class ComicParsingTests
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
private readonly DefaultParser _defaultParser;
|
||||
private static readonly string DefaultVolume = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume;
|
||||
private readonly IDefaultParser _basicParser;
|
||||
|
||||
public ComicParserTests(ITestOutputHelper testOutputHelper)
|
||||
public ComicParsingTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
_defaultParser =
|
||||
new DefaultParser(new DirectoryService(Substitute.For<ILogger<DirectoryService>>(),
|
||||
new MockFileSystem()));
|
||||
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
|
||||
_basicParser = new BasicParser(directoryService, new ImageParser(directoryService));
|
||||
}
|
||||
|
||||
[Theory]
|
107
API.Tests/Parsing/ImageParsingTests.cs
Normal file
107
API.Tests/Parsing/ImageParsingTests.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
using System.IO.Abstractions.TestingHelpers;
|
||||
using API.Entities.Enums;
|
||||
using API.Services;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace API.Tests.Parsing;
|
||||
|
||||
public class ImageParsingTests
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
private readonly ImageParser _parser;
|
||||
|
||||
public ImageParsingTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
var directoryService = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new MockFileSystem());
|
||||
_parser = new ImageParser(directoryService);
|
||||
}
|
||||
|
||||
//[Fact]
|
||||
public void Parse_ParseInfo_Manga_ImageOnly()
|
||||
{
|
||||
// Images don't have root path as E:\Manga, but rather as the path of the folder
|
||||
|
||||
// Note: Fallback to folder will parse Monster #8 and get Monster
|
||||
var filepath = @"E:\Manga\Monster #8\Ch. 001-016 [MangaPlus] [Digital] [amit34521]\Monster #8 Ch. 001 [MangaPlus] [Digital] [amit34521]\13.jpg";
|
||||
var expectedInfo2 = new ParserInfo
|
||||
{
|
||||
Series = "Monster #8", Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume, Edition = "",
|
||||
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
};
|
||||
var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, null);
|
||||
Assert.NotNull(actual2);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expectedInfo2.Format, actual2.Format);
|
||||
_testOutputHelper.WriteLine("Format ✓");
|
||||
Assert.Equal(expectedInfo2.Series, actual2.Series);
|
||||
_testOutputHelper.WriteLine("Series ✓");
|
||||
Assert.Equal(expectedInfo2.Chapters, actual2.Chapters);
|
||||
_testOutputHelper.WriteLine("Chapters ✓");
|
||||
Assert.Equal(expectedInfo2.Volumes, actual2.Volumes);
|
||||
_testOutputHelper.WriteLine("Volumes ✓");
|
||||
Assert.Equal(expectedInfo2.Edition, actual2.Edition);
|
||||
_testOutputHelper.WriteLine("Edition ✓");
|
||||
Assert.Equal(expectedInfo2.Filename, actual2.Filename);
|
||||
_testOutputHelper.WriteLine("Filename ✓");
|
||||
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
|
||||
_testOutputHelper.WriteLine("FullFilePath ✓");
|
||||
|
||||
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Vol19\ch. 186\Vol. 19 p106.gif";
|
||||
expectedInfo2 = new ParserInfo
|
||||
{
|
||||
Series = "Just Images the second", Volumes = "19", Edition = "",
|
||||
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
};
|
||||
|
||||
actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null);
|
||||
Assert.NotNull(actual2);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expectedInfo2.Format, actual2.Format);
|
||||
_testOutputHelper.WriteLine("Format ✓");
|
||||
Assert.Equal(expectedInfo2.Series, actual2.Series);
|
||||
_testOutputHelper.WriteLine("Series ✓");
|
||||
Assert.Equal(expectedInfo2.Chapters, actual2.Chapters);
|
||||
_testOutputHelper.WriteLine("Chapters ✓");
|
||||
Assert.Equal(expectedInfo2.Volumes, actual2.Volumes);
|
||||
_testOutputHelper.WriteLine("Volumes ✓");
|
||||
Assert.Equal(expectedInfo2.Edition, actual2.Edition);
|
||||
_testOutputHelper.WriteLine("Edition ✓");
|
||||
Assert.Equal(expectedInfo2.Filename, actual2.Filename);
|
||||
_testOutputHelper.WriteLine("Filename ✓");
|
||||
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
|
||||
_testOutputHelper.WriteLine("FullFilePath ✓");
|
||||
|
||||
filepath = @"E:\Manga\Extra layer for no reason\Just Images the second\Blank Folder\Vol19\ch. 186\Vol. 19 p106.gif";
|
||||
expectedInfo2 = new ParserInfo
|
||||
{
|
||||
Series = "Just Images the second", Volumes = "19", Edition = "",
|
||||
Chapters = "186", Filename = "Vol. 19 p106.gif", Format = MangaFormat.Image,
|
||||
FullFilePath = filepath, IsSpecial = false
|
||||
};
|
||||
|
||||
actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null);
|
||||
Assert.NotNull(actual2);
|
||||
_testOutputHelper.WriteLine($"Validating {filepath}");
|
||||
Assert.Equal(expectedInfo2.Format, actual2.Format);
|
||||
_testOutputHelper.WriteLine("Format ✓");
|
||||
Assert.Equal(expectedInfo2.Series, actual2.Series);
|
||||
_testOutputHelper.WriteLine("Series ✓");
|
||||
Assert.Equal(expectedInfo2.Chapters, actual2.Chapters);
|
||||
_testOutputHelper.WriteLine("Chapters ✓");
|
||||
Assert.Equal(expectedInfo2.Volumes, actual2.Volumes);
|
||||
_testOutputHelper.WriteLine("Volumes ✓");
|
||||
Assert.Equal(expectedInfo2.Edition, actual2.Edition);
|
||||
_testOutputHelper.WriteLine("Edition ✓");
|
||||
Assert.Equal(expectedInfo2.Filename, actual2.Filename);
|
||||
_testOutputHelper.WriteLine("Filename ✓");
|
||||
Assert.Equal(expectedInfo2.FullFilePath, actual2.FullFilePath);
|
||||
_testOutputHelper.WriteLine("FullFilePath ✓");
|
||||
}
|
||||
}
|
|
@ -2,13 +2,13 @@ using API.Entities.Enums;
|
|||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
namespace API.Tests.Parsing;
|
||||
|
||||
public class MangaParserTests
|
||||
public class MangaParsingTests
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
|
||||
public MangaParserTests(ITestOutputHelper testOutputHelper)
|
||||
public MangaParsingTests(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Xunit;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
namespace API.Tests.Parsing;
|
||||
|
||||
public class ParserInfoTests
|
||||
{
|
|
@ -3,9 +3,9 @@ using System.Linq;
|
|||
using Xunit;
|
||||
using static API.Services.Tasks.Scanner.Parser.Parser;
|
||||
|
||||
namespace API.Tests.Parser;
|
||||
namespace API.Tests.Parsing;
|
||||
|
||||
public class ParserTests
|
||||
public class ParsingTests
|
||||
{
|
||||
[Fact]
|
||||
public void ShouldWork()
|
|
@ -52,12 +52,12 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
|
|||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
||||
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public ParserInfo ParseFile(string path, string rootPath, LibraryType type)
|
||||
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
@ -156,7 +156,9 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(),
|
||||
Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
await ResetDB();
|
||||
var s = new SeriesBuilder("Test").Build();
|
||||
|
@ -231,7 +233,8 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cleanupService = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
cleanupService.CleanupChapters(new []{1, 3});
|
||||
Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories));
|
||||
|
@ -252,7 +255,8 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
var c = new ChapterBuilder("1")
|
||||
.WithFile(new MangaFileBuilder($"{DataDirectory}1.epub", MangaFormat.Epub).Build())
|
||||
|
@ -292,7 +296,8 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
|
@ -335,7 +340,8 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
|
@ -375,7 +381,8 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
|
@ -419,7 +426,8 @@ public class CacheServiceTests
|
|||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem);
|
||||
var cs = new CacheService(_logger, _unitOfWork, ds,
|
||||
new ReadingItemService(Substitute.For<IArchiveService>(),
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds), Substitute.For<IBookmarkService>());
|
||||
Substitute.For<IBookService>(), Substitute.For<IImageService>(), ds, Substitute.For<ILogger<ReadingItemService>>()),
|
||||
Substitute.For<IBookmarkService>());
|
||||
|
||||
// Flatten to prepare for how GetFullPath expects
|
||||
ds.Flatten($"{CacheDirectory}1/");
|
||||
|
|
|
@ -54,14 +54,14 @@ internal class MockReadingItemService : IReadingItemService
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ParserInfo Parse(string path, string rootPath, LibraryType type)
|
||||
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||
{
|
||||
return _defaultParser.Parse(path, rootPath, type);
|
||||
return _defaultParser.Parse(path, rootPath, libraryRoot, type);
|
||||
}
|
||||
|
||||
public ParserInfo ParseFile(string path, string rootPath, LibraryType type)
|
||||
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||
{
|
||||
return _defaultParser.Parse(path, rootPath, type);
|
||||
return _defaultParser.Parse(path, rootPath, libraryRoot, type);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +231,7 @@ public class ParseScannedFilesTests
|
|||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
||||
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
||||
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
|
||||
|
||||
var parsedSeries = new Dictionary<ParsedSeries, IList<ParserInfo>>();
|
||||
|
||||
|
@ -289,14 +289,14 @@ public class ParseScannedFilesTests
|
|||
var fileSystem = CreateTestFilesystem();
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
||||
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
||||
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
|
||||
|
||||
var directoriesSeen = new HashSet<string>();
|
||||
var library =
|
||||
await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
|
||||
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
|
||||
await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
|
||||
(files, directoryPath) =>
|
||||
(files, directoryPath, libraryFolder) =>
|
||||
{
|
||||
directoriesSeen.Add(directoryPath);
|
||||
return Task.CompletedTask;
|
||||
|
@ -311,11 +311,11 @@ public class ParseScannedFilesTests
|
|||
var fileSystem = CreateTestFilesystem();
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
||||
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
||||
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
|
||||
|
||||
var directoriesSeen = new HashSet<string>();
|
||||
await psf.ProcessFiles("C:/Data/", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
|
||||
(files, directoryPath) =>
|
||||
(files, directoryPath, libraryFolder) =>
|
||||
{
|
||||
directoriesSeen.Add(directoryPath);
|
||||
return Task.CompletedTask;
|
||||
|
@ -342,10 +342,11 @@ public class ParseScannedFilesTests
|
|||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
||||
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
||||
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
|
||||
|
||||
var callCount = 0;
|
||||
await psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) =>
|
||||
await psf.ProcessFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
|
||||
(files, folderPath, libraryFolder) =>
|
||||
{
|
||||
callCount++;
|
||||
|
||||
|
@ -375,10 +376,11 @@ public class ParseScannedFilesTests
|
|||
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
||||
new MockReadingItemService(new DefaultParser(ds)), Substitute.For<IEventHub>());
|
||||
new MockReadingItemService(new BasicParser(ds, new ImageParser(ds))), Substitute.For<IEventHub>());
|
||||
|
||||
var callCount = 0;
|
||||
await psf.ProcessFiles("C:/Data", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, folderPath) =>
|
||||
await psf.ProcessFiles("C:/Data", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
|
||||
(files, folderPath, libraryFolder) =>
|
||||
{
|
||||
callCount++;
|
||||
return Task.CompletedTask;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -48,6 +48,7 @@ public enum FilterField
|
|||
/// <summary>
|
||||
/// Average rating from Kavita+ - Not usable for non-licensed users
|
||||
/// </summary>
|
||||
AverageRating = 28
|
||||
AverageRating = 28,
|
||||
Imprint = 29
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ public class ChapterMetadataDto
|
|||
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Imprints { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
|
||||
|
|
|
@ -22,4 +22,5 @@ public class RelatedSeriesDto
|
|||
public IEnumerable<SeriesDto> Doujinshis { get; set; } = default!;
|
||||
public IEnumerable<SeriesDto> Parent { get; set; } = default!;
|
||||
public IEnumerable<SeriesDto> Editions { get; set; } = default!;
|
||||
public IEnumerable<SeriesDto> Annuals { get; set; } = default!;
|
||||
}
|
||||
|
|
|
@ -17,4 +17,5 @@ public class UpdateRelatedSeriesDto
|
|||
public IList<int> AlternativeVersions { get; set; } = default!;
|
||||
public IList<int> Doujinshis { get; set; } = default!;
|
||||
public IList<int> Editions { get; set; } = default!;
|
||||
public IList<int> Annuals { get; set; } = default!;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ public class SeriesMetadataDto
|
|||
public ICollection<PersonDto> Characters { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Pencillers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Inkers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Imprints { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Colorists { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Letterers { get; set; } = new List<PersonDto>();
|
||||
public ICollection<PersonDto> Editors { get; set; } = new List<PersonDto>();
|
||||
|
@ -80,6 +81,7 @@ public class SeriesMetadataDto
|
|||
public bool ColoristLocked { get; set; }
|
||||
public bool EditorLocked { get; set; }
|
||||
public bool InkerLocked { get; set; }
|
||||
public bool ImprintLocked { get; set; }
|
||||
public bool LettererLocked { get; set; }
|
||||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
|
|
|
@ -127,8 +127,10 @@ public class ComicInfo
|
|||
public string CoverArtist { get; set; } = string.Empty;
|
||||
public string Editor { get; set; } = string.Empty;
|
||||
public string Publisher { get; set; } = string.Empty;
|
||||
public string Imprint { get; set; } = string.Empty;
|
||||
public string Characters { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public static AgeRating ConvertAgeRatingToEnum(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return Entities.Enums.AgeRating.Unknown;
|
||||
|
@ -151,6 +153,7 @@ public class ComicInfo
|
|||
info.Letterer = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Letterer);
|
||||
info.Penciller = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Penciller);
|
||||
info.Publisher = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Publisher);
|
||||
info.Imprint = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Imprint);
|
||||
info.Characters = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Characters);
|
||||
info.Translator = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.Translator);
|
||||
info.CoverArtist = Services.Tasks.Scanner.Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||
|
|
2889
API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs
generated
Normal file
2889
API/Data/Migrations/20240309140117_SeriesImprints.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
29
API/Data/Migrations/20240309140117_SeriesImprints.cs
Normal file
29
API/Data/Migrations/20240309140117_SeriesImprints.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeriesImprints : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ImprintLocked",
|
||||
table: "SeriesMetadata",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ImprintLocked",
|
||||
table: "SeriesMetadata");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1253,6 +1253,9 @@ namespace API.Data.Migrations
|
|||
b.Property<bool>("GenresLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("ImprintLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("InkerLocked")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -1184,6 +1184,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
FilterField.Letterer => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Colorist => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Inker => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Imprint => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Penciller => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Writers => query.HasPeople(true, statement.Comparison, (IList<int>) value),
|
||||
FilterField.Genres => query.HasGenre(true, statement.Comparison, (IList<int>) value),
|
||||
|
@ -1818,19 +1819,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
AlternativeSettings = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeSetting, userRating),
|
||||
AlternativeVersions = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.AlternativeVersion, userRating),
|
||||
Doujinshis = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Doujinshi, userRating),
|
||||
// Parent = await _context.Series
|
||||
// .SelectMany(s =>
|
||||
// s.TargetSeries.Where(r => r.TargetSeriesId == seriesId
|
||||
// && usersSeriesIds.Contains(r.TargetSeriesId)
|
||||
// && r.RelationKind != RelationKind.Prequel
|
||||
// && r.RelationKind != RelationKind.Sequel
|
||||
// && r.RelationKind != RelationKind.Edition)
|
||||
// .Select(sr => sr.Series))
|
||||
// .RestrictAgainstAgeRestriction(userRating)
|
||||
// .AsSplitQuery()
|
||||
// .AsNoTracking()
|
||||
// .ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
// .ToListAsync(),
|
||||
Annuals = await GetRelatedSeriesQuery(seriesId, usersSeriesIds, RelationKind.Annual, userRating),
|
||||
Parent = await _context.SeriesRelation
|
||||
.Where(r => r.TargetSeriesId == seriesId
|
||||
&& usersSeriesIds.Contains(r.TargetSeriesId)
|
||||
|
|
|
@ -29,4 +29,10 @@ public enum LibraryType
|
|||
/// </summary>
|
||||
[Description("Light Novel")]
|
||||
LightNovel = 4,
|
||||
/// <summary>
|
||||
/// Uses Comic regex for filename parsing, uses ComicVine type of Parsing. Will replace Comic type in future
|
||||
/// </summary>
|
||||
[Description("Comic (ComicVine)")]
|
||||
ComicVine = 5,
|
||||
|
||||
}
|
||||
|
|
|
@ -24,7 +24,11 @@ public enum PersonRole
|
|||
/// <summary>
|
||||
/// The Translator
|
||||
/// </summary>
|
||||
Translator = 12
|
||||
Translator = 12,
|
||||
/// <summary>
|
||||
/// The publisher before another Publisher bought
|
||||
/// </summary>
|
||||
Imprint = 13
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -71,6 +71,11 @@ public enum RelationKind
|
|||
/// Same story, could be translation, colorization... Different edition of the series
|
||||
/// </summary>
|
||||
[Description("Edition")]
|
||||
Edition = 13
|
||||
Edition = 13,
|
||||
/// <summary>
|
||||
/// The target series is an annual of the Series
|
||||
/// </summary>
|
||||
[Description("Annual")]
|
||||
Annual = 14
|
||||
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ public class SeriesMetadata : IHasConcurrencyToken
|
|||
public bool ColoristLocked { get; set; }
|
||||
public bool EditorLocked { get; set; }
|
||||
public bool InkerLocked { get; set; }
|
||||
public bool ImprintLocked { get; set; }
|
||||
public bool LettererLocked { get; set; }
|
||||
public bool PencillerLocked { get; set; }
|
||||
public bool PublisherLocked { get; set; }
|
||||
|
|
|
@ -154,6 +154,9 @@ public class AutoMapperProfiles : Profile
|
|||
.ForMember(dest => dest.Inkers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Imprints,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Imprint).OrderBy(p => p.NormalizedName)))
|
||||
.ForMember(dest => dest.Letterers,
|
||||
opt =>
|
||||
opt.MapFrom(src => src.People.Where(p => p.Role == PersonRole.Letterer).OrderBy(p => p.NormalizedName)))
|
||||
|
|
|
@ -58,6 +58,9 @@ public static class FilterFieldValueConverter
|
|||
FilterField.Inker => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Imprint => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
FilterField.Penciller => value.Split(',')
|
||||
.Select(int.Parse)
|
||||
.ToList(),
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
#nullable enable
|
||||
|
@ -12,7 +13,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, LibraryType type);
|
||||
ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type);
|
||||
}
|
||||
|
||||
public class ReadingItemService : IReadingItemService
|
||||
|
@ -21,16 +22,27 @@ public class ReadingItemService : IReadingItemService
|
|||
private readonly IBookService _bookService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IDefaultParser _defaultParser;
|
||||
private readonly ILogger<ReadingItemService> _logger;
|
||||
private readonly BasicParser _basicParser;
|
||||
private readonly ComicVineParser _comicVineParser;
|
||||
private readonly ImageParser _imageParser;
|
||||
private readonly BookParser _bookParser;
|
||||
private readonly PdfParser _pdfParser;
|
||||
|
||||
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService, IDirectoryService directoryService)
|
||||
public ReadingItemService(IArchiveService archiveService, IBookService bookService, IImageService imageService,
|
||||
IDirectoryService directoryService, ILogger<ReadingItemService> logger)
|
||||
{
|
||||
_archiveService = archiveService;
|
||||
_bookService = bookService;
|
||||
_imageService = imageService;
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
|
||||
_defaultParser = new DefaultParser(directoryService);
|
||||
_comicVineParser = new ComicVineParser(directoryService);
|
||||
_imageParser = new ImageParser(directoryService);
|
||||
_bookParser = new BookParser(directoryService, bookService, _basicParser);
|
||||
_pdfParser = new PdfParser(directoryService);
|
||||
_basicParser = new BasicParser(directoryService, _imageParser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -59,37 +71,15 @@ 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, LibraryType type)
|
||||
public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||
{
|
||||
var info = Parse(path, rootPath, type);
|
||||
var info = Parse(path, rootPath, libraryRoot, type);
|
||||
if (info == null)
|
||||
{
|
||||
_logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// This catches when original library type is Manga/Comic and when parsing with non
|
||||
if (Parser.IsEpub(path) && Parser.ParseVolume(info.Series) != Parser.LooseLeafVolume) // Shouldn't this be info.Volume != DefaultVolume?
|
||||
{
|
||||
var hasVolumeInTitle = !Parser.ParseVolume(info.Title)
|
||||
.Equals(Parser.LooseLeafVolume);
|
||||
var hasVolumeInSeries = !Parser.ParseVolume(info.Series)
|
||||
.Equals(Parser.LooseLeafVolume);
|
||||
|
||||
if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series)))
|
||||
{
|
||||
// This is likely a light novel for which we can set series from parsed title
|
||||
info.Series = Parser.ParseSeries(info.Title);
|
||||
info.Volumes = Parser.ParseVolume(info.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
var info2 = _defaultParser.Parse(path, rootPath, LibraryType.Book);
|
||||
info.Merge(info2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
@ -176,55 +166,29 @@ public class ReadingItemService : IReadingItemService
|
|||
/// <param name="rootPath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
private ParserInfo? Parse(string path, string rootPath, LibraryType type)
|
||||
private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type)
|
||||
{
|
||||
var info = Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type);
|
||||
|
||||
if (info == null) return null;
|
||||
|
||||
info.ComicInfo = GetComicInfo(path);
|
||||
if (info.ComicInfo == null) return info;
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Volume))
|
||||
if (_comicVineParser.IsApplicable(path, type))
|
||||
{
|
||||
info.Volumes = info.ComicInfo.Volume;
|
||||
return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Series))
|
||||
if (_imageParser.IsApplicable(path, type))
|
||||
{
|
||||
info.Series = info.ComicInfo.Series.Trim();
|
||||
return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
|
||||
if (_bookParser.IsApplicable(path, type))
|
||||
{
|
||||
info.Chapters = info.ComicInfo.Number;
|
||||
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
|
||||
{
|
||||
info.IsSpecial = false;
|
||||
}
|
||||
return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
||||
}
|
||||
if (_pdfParser.IsApplicable(path, type))
|
||||
{
|
||||
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
||||
}
|
||||
if (_basicParser.IsApplicable(path, type))
|
||||
{
|
||||
return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path));
|
||||
}
|
||||
|
||||
// Patch is SeriesSort from ComicInfo
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format))
|
||||
{
|
||||
info.IsSpecial = true;
|
||||
info.Chapters = Parser.DefaultChapter;
|
||||
info.Volumes = Parser.LooseLeafVolume;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.SeriesSort.Trim();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.LocalizedSeries))
|
||||
{
|
||||
info.LocalizedSeries = info.ComicInfo.LocalizedSeries.Trim();
|
||||
}
|
||||
|
||||
return info;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ public interface ISeriesService
|
|||
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? chapterTitle,
|
||||
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);
|
||||
Task<NextExpectedChapterDto> GetEstimatedChapterCreationDate(int seriesId, int userId);
|
||||
|
@ -171,7 +171,7 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
|
||||
if (updateSeriesMetadataDto.CollectionTags.Any())
|
||||
if (updateSeriesMetadataDto.CollectionTags.Count > 0)
|
||||
{
|
||||
var allCollectionTags = (await _unitOfWork.CollectionTagRepository
|
||||
.GetAllTagsByNamesAsync(updateSeriesMetadataDto.CollectionTags.Select(t => Parser.Normalize(t.Title)))).ToList();
|
||||
|
@ -195,7 +195,7 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
|
||||
if (updateSeriesMetadataDto.SeriesMetadata?.Tags != null && updateSeriesMetadataDto.SeriesMetadata.Tags.Any())
|
||||
if (updateSeriesMetadataDto.SeriesMetadata?.Tags is {Count: > 0})
|
||||
{
|
||||
var allTags = (await _unitOfWork.TagRepository
|
||||
.GetAllTagsByNameAsync(updateSeriesMetadataDto.SeriesMetadata.Tags.Select(t => Parser.Normalize(t.Title))))
|
||||
|
@ -207,68 +207,72 @@ public class SeriesService : ISeriesService
|
|||
}, () => series.Metadata.TagsLocked = true);
|
||||
}
|
||||
|
||||
|
||||
if (PersonHelper.HasAnyPeople(updateSeriesMetadataDto.SeriesMetadata))
|
||||
{
|
||||
void HandleAddPerson(Person person)
|
||||
{
|
||||
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
|
||||
}
|
||||
|
||||
series.Metadata.People ??= new List<Person>();
|
||||
var allWriters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Writer,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Writers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata!.Writers, series, allWriters.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.WriterLocked = true);
|
||||
|
||||
var allCharacters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Character,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Characters.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allCharacters.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.CharacterLocked = true);
|
||||
|
||||
var allColorists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Colorist,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Colorists.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allColorists.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.ColoristLocked = true);
|
||||
|
||||
var allEditors = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Editor,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Editors.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allEditors.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.EditorLocked = true);
|
||||
|
||||
var allInkers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Inker,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Inkers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allInkers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.InkerLocked = true);
|
||||
|
||||
var allLetterers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Letterer,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Letterers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allLetterers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.LettererLocked = true);
|
||||
|
||||
var allPencillers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Penciller,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Pencillers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPencillers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.PencillerLocked = true);
|
||||
|
||||
var allPublishers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Publisher,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Publishers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPublishers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.PublisherLocked = true);
|
||||
|
||||
var allTranslators = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Translator,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Translators.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allTranslators.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.TranslatorLocked = true);
|
||||
|
||||
var allCoverArtists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.CoverArtist,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.CoverArtists.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allCoverArtists.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.CoverArtistLocked = true);
|
||||
}
|
||||
|
||||
if (updateSeriesMetadataDto.SeriesMetadata != null)
|
||||
{
|
||||
if (PersonHelper.HasAnyPeople(updateSeriesMetadataDto.SeriesMetadata))
|
||||
{
|
||||
void HandleAddPerson(Person person)
|
||||
{
|
||||
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
|
||||
}
|
||||
|
||||
series.Metadata.People ??= new List<Person>();
|
||||
var allWriters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Writer,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Writers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Writer, updateSeriesMetadataDto.SeriesMetadata.Writers, series, allWriters.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.WriterLocked = true);
|
||||
|
||||
var allCharacters = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Character,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Characters.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Character, updateSeriesMetadataDto.SeriesMetadata.Characters, series, allCharacters.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.CharacterLocked = true);
|
||||
|
||||
var allColorists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Colorist,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Colorists.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Colorist, updateSeriesMetadataDto.SeriesMetadata.Colorists, series, allColorists.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.ColoristLocked = true);
|
||||
|
||||
var allEditors = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Editor,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Editors.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Editor, updateSeriesMetadataDto.SeriesMetadata.Editors, series, allEditors.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.EditorLocked = true);
|
||||
|
||||
var allInkers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Inker,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Inkers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Inker, updateSeriesMetadataDto.SeriesMetadata.Inkers, series, allInkers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.InkerLocked = true);
|
||||
|
||||
var allLetterers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Letterer,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Letterers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Letterer, updateSeriesMetadataDto.SeriesMetadata.Letterers, series, allLetterers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.LettererLocked = true);
|
||||
|
||||
var allPencillers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Penciller,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Pencillers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Penciller, updateSeriesMetadataDto.SeriesMetadata.Pencillers, series, allPencillers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.PencillerLocked = true);
|
||||
|
||||
var allPublishers = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Publisher,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Publishers.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Publisher, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allPublishers.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.PublisherLocked = true);
|
||||
|
||||
var allImprints = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Imprint,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Imprints.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Imprint, updateSeriesMetadataDto.SeriesMetadata.Publishers, series, allImprints.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.ImprintLocked = true);
|
||||
|
||||
var allTranslators = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.Translator,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.Translators.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.Translator, updateSeriesMetadataDto.SeriesMetadata.Translators, series, allTranslators.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.TranslatorLocked = true);
|
||||
|
||||
var allCoverArtists = await _unitOfWork.PersonRepository.GetAllPeopleByRoleAndNames(PersonRole.CoverArtist,
|
||||
updateSeriesMetadataDto.SeriesMetadata!.CoverArtists.Select(p => Parser.Normalize(p.Name)));
|
||||
PersonHelper.UpdatePeopleList(PersonRole.CoverArtist, updateSeriesMetadataDto.SeriesMetadata.CoverArtists, series, allCoverArtists.AsReadOnly(),
|
||||
HandleAddPerson, () => series.Metadata.CoverArtistLocked = true);
|
||||
}
|
||||
|
||||
series.Metadata.AgeRatingLocked = updateSeriesMetadataDto.SeriesMetadata.AgeRatingLocked;
|
||||
series.Metadata.PublicationStatusLocked = updateSeriesMetadataDto.SeriesMetadata.PublicationStatusLocked;
|
||||
series.Metadata.LanguageLocked = updateSeriesMetadataDto.SeriesMetadata.LanguageLocked;
|
||||
|
@ -278,6 +282,7 @@ public class SeriesService : ISeriesService
|
|||
series.Metadata.ColoristLocked = updateSeriesMetadataDto.SeriesMetadata.ColoristLocked;
|
||||
series.Metadata.EditorLocked = updateSeriesMetadataDto.SeriesMetadata.EditorLocked;
|
||||
series.Metadata.InkerLocked = updateSeriesMetadataDto.SeriesMetadata.InkerLocked;
|
||||
series.Metadata.ImprintLocked = updateSeriesMetadataDto.SeriesMetadata.ImprintLocked;
|
||||
series.Metadata.LettererLocked = updateSeriesMetadataDto.SeriesMetadata.LettererLocked;
|
||||
series.Metadata.PencillerLocked = updateSeriesMetadataDto.SeriesMetadata.PencillerLocked;
|
||||
series.Metadata.PublisherLocked = updateSeriesMetadataDto.SeriesMetadata.PublisherLocked;
|
||||
|
@ -519,8 +524,10 @@ public class SeriesService : ISeriesService
|
|||
|
||||
foreach (var chapter in chapters)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
|
||||
else chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
|
||||
// if (!string.IsNullOrEmpty(chapter.TitleName)) chapter.Title = chapter.TitleName;
|
||||
// else chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
|
||||
|
||||
chapter.Title = await FormatChapterTitle(userId, chapter, libraryType);
|
||||
|
||||
if (!chapter.IsSpecial) continue;
|
||||
specials.Add(chapter);
|
||||
|
@ -563,7 +570,6 @@ public class SeriesService : ISeriesService
|
|||
|
||||
public static bool RenameVolumeName(VolumeDto volume, LibraryType libraryType, string volumeLabel = "Volume")
|
||||
{
|
||||
// TODO: Move this into DB (not sure how because of localization and lookups)
|
||||
if (libraryType is LibraryType.Book or LibraryType.LightNovel)
|
||||
{
|
||||
var firstChapter = volume.Chapters.First();
|
||||
|
@ -590,36 +596,47 @@ public class SeriesService : ISeriesService
|
|||
}
|
||||
|
||||
|
||||
public async Task<string> FormatChapterTitle(int userId, bool isSpecial, LibraryType libraryType, 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)) throw new ArgumentException("Chapter Title cannot be null");
|
||||
if (string.IsNullOrEmpty(chapterTitle) && (isSpecial || libraryType == LibraryType.Book)) throw new ArgumentException("Chapter Title cannot be null");
|
||||
|
||||
if (isSpecial)
|
||||
{
|
||||
return Parser.CleanSpecialTitle(chapterTitle);
|
||||
return Parser.CleanSpecialTitle(chapterTitle!);
|
||||
}
|
||||
|
||||
var hashSpot = withHash ? "#" : string.Empty;
|
||||
return libraryType switch
|
||||
var baseChapter = libraryType switch
|
||||
{
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle),
|
||||
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterTitle),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, chapterTitle),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", chapterTitle),
|
||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", chapterTitle!),
|
||||
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", chapterRange),
|
||||
LibraryType.Comic => 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.Image => await _localizationService.Translate(userId, "chapter-num", chapterRange),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(chapterTitle) && libraryType != LibraryType.Book && chapterTitle != chapterRange)
|
||||
{
|
||||
baseChapter += " - " + chapterTitle;
|
||||
}
|
||||
|
||||
|
||||
return baseChapter;
|
||||
}
|
||||
|
||||
public async Task<string> FormatChapterTitle(int userId, ChapterDto chapter, LibraryType libraryType, bool withHash = true)
|
||||
{
|
||||
return await FormatChapterTitle(userId, chapter.IsSpecial, libraryType, 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.Title, withHash);
|
||||
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)
|
||||
{
|
||||
var hashSpot = withHash ? "#" : string.Empty;
|
||||
|
@ -628,6 +645,7 @@ public class SeriesService : ISeriesService
|
|||
LibraryType.Book => await _localizationService.Translate(userId, "book-num", string.Empty),
|
||||
LibraryType.LightNovel => await _localizationService.Translate(userId, "book-num", string.Empty),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
|
||||
LibraryType.ComicVine => await _localizationService.Translate(userId, "issue-num", hashSpot, string.Empty),
|
||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", string.Empty),
|
||||
_ => await _localizationService.Translate(userId, "chapter-num", ' ')
|
||||
}).Trim();
|
||||
|
@ -666,6 +684,7 @@ public class SeriesService : ISeriesService
|
|||
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
|
||||
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
|
||||
UpdateRelationForKind(dto.Editions, series.Relations.Where(r => r.RelationKind == RelationKind.Edition).ToList(), series, RelationKind.Edition);
|
||||
UpdateRelationForKind(dto.Annuals, series.Relations.Where(r => r.RelationKind == RelationKind.Annual).ToList(), series, RelationKind.Annual);
|
||||
|
||||
if (!_unitOfWork.HasChanges()) return true;
|
||||
return await _unitOfWork.CommitAsync();
|
||||
|
|
|
@ -45,8 +45,6 @@ public class BackupService : IBackupService
|
|||
_backupFiles = new List<string>()
|
||||
{
|
||||
"appsettings.json",
|
||||
"Hangfire.db", // This is not used atm
|
||||
"Hangfire-log.db", // This is not used atm
|
||||
"kavita.db",
|
||||
"kavita.db-shm", // This wont always be there
|
||||
"kavita.db-wal" // This wont always be there
|
||||
|
@ -109,19 +107,21 @@ public class BackupService : IBackupService
|
|||
_directoryService.CopyFilesToDirectory(
|
||||
_backupFiles.Select(file => _directoryService.FileSystem.Path.Join(_directoryService.ConfigDirectory, file)).ToList(), tempDirectory);
|
||||
|
||||
await SendProgress(0.2F, "Copying logs");
|
||||
CopyLogsToBackupDirectory(tempDirectory);
|
||||
|
||||
await SendProgress(0.25F, "Copying cover images");
|
||||
|
||||
await CopyCoverImagesToBackupDirectory(tempDirectory);
|
||||
|
||||
await SendProgress(0.5F, "Copying bookmarks");
|
||||
await SendProgress(0.35F, "Copying templates images");
|
||||
CopyTemplatesToBackupDirectory(tempDirectory);
|
||||
|
||||
await SendProgress(0.5F, "Copying bookmarks");
|
||||
await CopyBookmarksToBackupDirectory(tempDirectory);
|
||||
|
||||
await SendProgress(0.75F, "Copying themes");
|
||||
|
||||
CopyThemesToBackupDirectory(tempDirectory);
|
||||
|
||||
await SendProgress(0.85F, "Copying favicons");
|
||||
CopyFaviconsToBackupDirectory(tempDirectory);
|
||||
|
||||
|
@ -150,6 +150,11 @@ public class BackupService : IBackupService
|
|||
_directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons"));
|
||||
}
|
||||
|
||||
private void CopyTemplatesToBackupDirectory(string tempDirectory)
|
||||
{
|
||||
_directoryService.CopyDirectoryToDirectory(_directoryService.TemplateDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "templates"));
|
||||
}
|
||||
|
||||
private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)
|
||||
{
|
||||
var outputTempDir = Path.Join(tempDirectory, "covers");
|
||||
|
|
|
@ -78,7 +78,7 @@ public class ParseScannedFiles
|
|||
/// <param name="folderAction">A callback async Task to be called once all files for each folder path are found</param>
|
||||
/// <param name="forceCheck">If we should bypass any folder last write time checks on the scan and force I/O</param>
|
||||
public async Task ProcessFiles(string folderPath, bool scanDirectoryByDirectory,
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<IList<string>, string,Task> folderAction, Library library, bool forceCheck = false)
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<IList<string>, string, string, Task> folderAction, Library library, bool forceCheck = false)
|
||||
{
|
||||
string normalizedPath;
|
||||
var fileExtensions = string.Join("|", library.LibraryFileTypes.Select(l => l.FileTypeGroup.GetRegex()));
|
||||
|
@ -110,12 +110,12 @@ public class ParseScannedFiles
|
|||
normalizedPath = Parser.Parser.NormalizePath(directory);
|
||||
if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, normalizedPath, forceCheck))
|
||||
{
|
||||
await folderAction(new List<string>(), directory);
|
||||
await folderAction(new List<string>(), directory, folderPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For a scan, this is doing everything in the directory loop before the folder Action is called...which leads to no progress indication
|
||||
await folderAction(_directoryService.ScanFiles(directory, fileExtensions, matcher), directory);
|
||||
await folderAction(_directoryService.ScanFiles(directory, fileExtensions, matcher), directory, folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,13 +125,13 @@ public class ParseScannedFiles
|
|||
normalizedPath = Parser.Parser.NormalizePath(folderPath);
|
||||
if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, normalizedPath, forceCheck))
|
||||
{
|
||||
await folderAction(new List<string>(), folderPath);
|
||||
await folderAction(new List<string>(), folderPath, folderPath);
|
||||
return;
|
||||
}
|
||||
// We need to calculate all folders till library root and see if any kavitaignores
|
||||
var seriesMatcher = BuildIgnoreFromLibraryRoot(folderPath, seriesPaths);
|
||||
|
||||
await folderAction(_directoryService.ScanFiles(folderPath, fileExtensions, seriesMatcher), folderPath);
|
||||
await folderAction(_directoryService.ScanFiles(folderPath, fileExtensions, seriesMatcher), folderPath, folderPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -313,7 +313,7 @@ public class ParseScannedFiles
|
|||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", library.Name, ProgressEventType.Ended));
|
||||
|
||||
async Task ProcessFolder(IList<string> files, string folder)
|
||||
async Task ProcessFolder(IList<string> files, string folder, string libraryRoot)
|
||||
{
|
||||
var normalizedFolder = Parser.Parser.NormalizePath(folder);
|
||||
if (HasSeriesFolderNotChangedSinceLastScan(seriesPaths, normalizedFolder, forceCheck))
|
||||
|
@ -342,7 +342,7 @@ public class ParseScannedFiles
|
|||
|
||||
var scannedSeries = new ConcurrentDictionary<ParsedSeries, List<ParserInfo>>();
|
||||
var infos = files
|
||||
.Select(file => _readingItemService.ParseFile(file, folder, library.Type))
|
||||
.Select(file => _readingItemService.ParseFile(file, folder, libraryRoot, library.Type))
|
||||
.Where(info => info != null)
|
||||
.ToList();
|
||||
|
||||
|
@ -416,7 +416,7 @@ public class ParseScannedFiles
|
|||
}
|
||||
else
|
||||
{
|
||||
// TODO: I think I need to bump by 0.1f as if the prevIssue matches counter
|
||||
// I need to bump by 0.1f as if the prevIssue matches counter
|
||||
if (!string.IsNullOrEmpty(prevIssue) && prevIssue == counter + "")
|
||||
{
|
||||
// Bump by 0.1
|
||||
|
|
114
API/Services/Tasks/Scanner/Parser/BasicParser.cs
Normal file
114
API/Services/Tasks/Scanner/Parser/BasicParser.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using System.IO;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Services.Tasks.Scanner.Parser;
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// This is the basic parser for handling Manga/Comic/Book libraries. This was previously DefaultParser before splitting each parser
|
||||
/// into their own classes.
|
||||
/// </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)
|
||||
{
|
||||
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.
|
||||
if (type != LibraryType.Image && Parser.IsCoverImage(directoryService.FileSystem.Path.GetFileName(filePath))) return null;
|
||||
|
||||
if (Parser.IsImage(filePath))
|
||||
{
|
||||
return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, comicInfo);
|
||||
}
|
||||
|
||||
var ret = new ParserInfo()
|
||||
{
|
||||
Filename = Path.GetFileName(filePath),
|
||||
Format = Parser.ParseFormat(filePath),
|
||||
Title = Parser.RemoveExtensionIfSupported(fileName),
|
||||
FullFilePath = filePath,
|
||||
Series = string.Empty,
|
||||
ComicInfo = comicInfo
|
||||
};
|
||||
|
||||
// This will be called if the epub is already parsed once then we call and merge the information, if the
|
||||
if (Parser.IsEpub(filePath))
|
||||
{
|
||||
ret.Chapters = Parser.ParseChapter(fileName);
|
||||
ret.Series = Parser.ParseSeries(fileName);
|
||||
ret.Volumes = Parser.ParseVolume(fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret.Chapters = type == LibraryType.Comic
|
||||
? Parser.ParseComicChapter(fileName)
|
||||
: Parser.ParseChapter(fileName);
|
||||
ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName);
|
||||
ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName);
|
||||
}
|
||||
|
||||
if (ret.Series == string.Empty || Parser.IsImage(filePath))
|
||||
{
|
||||
// Try to parse information out of each folder all the way to rootPath
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
var edition = Parser.ParseEdition(fileName);
|
||||
if (!string.IsNullOrEmpty(edition))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic);
|
||||
ret.Edition = edition;
|
||||
}
|
||||
|
||||
var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName);
|
||||
// We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that
|
||||
// could cause a problem as Omake is a special term, but there is valid volume/chapter information.
|
||||
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial)
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder
|
||||
}
|
||||
|
||||
// If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name
|
||||
if (Parser.HasSpecialMarker(fileName))
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ret.SpecialIndex = Parser.ParseSpecialIndex(fileName);
|
||||
ret.Chapters = Parser.DefaultChapter;
|
||||
ret.Volumes = Parser.SpecialVolume;
|
||||
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ret.Series))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic);
|
||||
}
|
||||
|
||||
// Pdfs may have .pdf in the series name, remove that
|
||||
if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf"))
|
||||
{
|
||||
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
|
||||
}
|
||||
|
||||
// v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number
|
||||
if (ret.IsSpecial)
|
||||
{
|
||||
ret.Volumes = Parser.SpecialVolume;
|
||||
}
|
||||
|
||||
return ret.Series == string.Empty ? null : ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applicable for everything but ComicVine and Image library types
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public override bool IsApplicable(string filePath, LibraryType type)
|
||||
{
|
||||
return type != LibraryType.ComicVine && type != LibraryType.Image;
|
||||
}
|
||||
}
|
47
API/Services/Tasks/Scanner/Parser/BookParser.cs
Normal file
47
API/Services/Tasks/Scanner/Parser/BookParser.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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)
|
||||
{
|
||||
var info = bookService.ParseInfo(filePath);
|
||||
if (info == null) return null;
|
||||
|
||||
// This catches when original library type is Manga/Comic and when parsing with non
|
||||
if (Parser.ParseVolume(info.Series) != Parser.LooseLeafVolume) // Shouldn't this be info.Volume != DefaultVolume?
|
||||
{
|
||||
var hasVolumeInTitle = !Parser.ParseVolume(info.Title)
|
||||
.Equals(Parser.LooseLeafVolume);
|
||||
var hasVolumeInSeries = !Parser.ParseVolume(info.Series)
|
||||
.Equals(Parser.LooseLeafVolume);
|
||||
|
||||
if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series)))
|
||||
{
|
||||
// This is likely a light novel for which we can set series from parsed title
|
||||
info.Series = Parser.ParseSeries(info.Title);
|
||||
info.Volumes = Parser.ParseVolume(info.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo);
|
||||
info.Merge(info2);
|
||||
}
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(info.Series) ? null : info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only applicable for Epub files
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public override bool IsApplicable(string filePath, LibraryType type)
|
||||
{
|
||||
return Parser.IsEpub(filePath);
|
||||
}
|
||||
}
|
130
API/Services/Tasks/Scanner/Parser/ComicVineParser.cs
Normal file
130
API/Services/Tasks/Scanner/Parser/ComicVineParser.cs
Normal file
|
@ -0,0 +1,130 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Services.Tasks.Scanner.Parser;
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for Parsing ComicVine Comics.
|
||||
/// </summary>
|
||||
/// <param name="directoryService"></param>
|
||||
public class ComicVineParser(IDirectoryService directoryService) : DefaultParser(directoryService)
|
||||
{
|
||||
/// <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)
|
||||
{
|
||||
if (type != LibraryType.ComicVine) return null;
|
||||
|
||||
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
||||
var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
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)
|
||||
};
|
||||
|
||||
// See if we can formulate the name from the ComicInfo
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo?.Series) && !string.IsNullOrEmpty(info.ComicInfo?.Volume))
|
||||
{
|
||||
info.Series = $"{info.ComicInfo.Series} ({info.ComicInfo.Volume})";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(info.Series))
|
||||
{
|
||||
// Check if we need to fallback to the Folder name AND that the folder matches the format "Series (Year)"
|
||||
var directories = directoryService.GetFoldersTillRoot(rootPath, filePath).ToList();
|
||||
if (directories.Count > 0)
|
||||
{
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
if (!Parser.IsSeriesAndYear(directory)) continue;
|
||||
info.Series = directory;
|
||||
info.Volumes = Parser.ParseYear(directory);
|
||||
break;
|
||||
}
|
||||
|
||||
// When there was at least one directory and we failed to parse the series, this is the final fallback
|
||||
if (string.IsNullOrEmpty(info.Series))
|
||||
{
|
||||
info.Series = Parser.CleanTitle(directories[0], true, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Parser.IsSeriesAndYear(directoryName))
|
||||
{
|
||||
info.Series = directoryName;
|
||||
info.Volumes = Parser.ParseYear(directoryName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a Special
|
||||
info.IsSpecial = Parser.IsComicSpecial(info.Filename) || Parser.IsComicSpecial(info.ComicInfo?.Format);
|
||||
|
||||
// Patch in other information from ComicInfo
|
||||
UpdateFromComicInfo(info);
|
||||
|
||||
if (string.IsNullOrEmpty(info.Series))
|
||||
{
|
||||
info.Series = Parser.CleanTitle(directoryName, true, true);
|
||||
}
|
||||
|
||||
|
||||
return string.IsNullOrEmpty(info.Series) ? null : info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only applicable for ComicVine library type
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public override bool IsApplicable(string filePath, LibraryType type)
|
||||
{
|
||||
return type == LibraryType.ComicVine;
|
||||
}
|
||||
|
||||
private void UpdateFromComicInfo(ParserInfo info)
|
||||
{
|
||||
if (info.ComicInfo == null) return;
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Volume))
|
||||
{
|
||||
info.Volumes = info.ComicInfo.Volume;
|
||||
}
|
||||
if (string.IsNullOrEmpty(info.Series) && !string.IsNullOrEmpty(info.ComicInfo.Series))
|
||||
{
|
||||
info.Series = info.ComicInfo.Series.Trim();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.Number))
|
||||
{
|
||||
info.Chapters = info.ComicInfo.Number;
|
||||
if (info.IsSpecial && Parser.DefaultChapter != info.Chapters)
|
||||
{
|
||||
info.IsSpecial = false;
|
||||
info.Volumes = $"{Parser.SpecialVolumeNumber}";
|
||||
}
|
||||
}
|
||||
|
||||
// Patch is SeriesSort from ComicInfo
|
||||
if (!string.IsNullOrEmpty(info.ComicInfo.TitleSort))
|
||||
{
|
||||
info.SeriesSort = info.ComicInfo.TitleSort.Trim();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Services.Tasks.Scanner.Parser;
|
||||
|
@ -7,165 +8,26 @@ namespace API.Services.Tasks.Scanner.Parser;
|
|||
|
||||
public interface IDefaultParser
|
||||
{
|
||||
ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga);
|
||||
ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null);
|
||||
void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret);
|
||||
bool IsApplicable(string filePath, LibraryType type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is an implementation of the Parser that is the basis for everything
|
||||
/// </summary>
|
||||
public class DefaultParser : IDefaultParser
|
||||
public abstract class DefaultParser(IDirectoryService directoryService) : IDefaultParser
|
||||
{
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
public DefaultParser(IDirectoryService directoryService)
|
||||
{
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses information out of a file path. Will fallback to using directory name if Series couldn't be parsed
|
||||
/// Parses information out of a file path. Can fallback to using directory name if Series couldn't be parsed
|
||||
/// from filename.
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="rootPath">Root folder</param>
|
||||
/// <param name="type">Defaults to Manga. Allows different Regex to be used for parsing.</param>
|
||||
/// <param name="type">Allows different Regex to be used for parsing.</param>
|
||||
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
|
||||
public ParserInfo? Parse(string filePath, string rootPath, LibraryType type = LibraryType.Manga)
|
||||
{
|
||||
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.
|
||||
if (type != LibraryType.Image && Parser.IsCoverImage(_directoryService.FileSystem.Path.GetFileName(filePath))) return null;
|
||||
|
||||
var ret = new ParserInfo()
|
||||
{
|
||||
Filename = Path.GetFileName(filePath),
|
||||
Format = Parser.ParseFormat(filePath),
|
||||
Title = Path.GetFileNameWithoutExtension(fileName),
|
||||
FullFilePath = filePath,
|
||||
Series = string.Empty
|
||||
};
|
||||
|
||||
// If library type is Image or this is not a cover image in a non-image library, then use dedicated parsing mechanism
|
||||
if (type == LibraryType.Image || Parser.IsImage(filePath))
|
||||
{
|
||||
// TODO: We can move this up one level
|
||||
return ParseImage(filePath, rootPath, ret);
|
||||
}
|
||||
|
||||
|
||||
// This will be called if the epub is already parsed once then we call and merge the information, if the
|
||||
if (Parser.IsEpub(filePath))
|
||||
{
|
||||
ret.Chapters = Parser.ParseChapter(fileName);
|
||||
ret.Series = Parser.ParseSeries(fileName);
|
||||
ret.Volumes = Parser.ParseVolume(fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret.Chapters = type == LibraryType.Comic
|
||||
? Parser.ParseComicChapter(fileName)
|
||||
: Parser.ParseChapter(fileName);
|
||||
ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName);
|
||||
ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName);
|
||||
}
|
||||
|
||||
if (ret.Series == string.Empty || Parser.IsImage(filePath))
|
||||
{
|
||||
// Try to parse information out of each folder all the way to rootPath
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
var edition = Parser.ParseEdition(fileName);
|
||||
if (!string.IsNullOrEmpty(edition))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic);
|
||||
ret.Edition = edition;
|
||||
}
|
||||
|
||||
var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName);
|
||||
// We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that
|
||||
// could cause a problem as Omake is a special term, but there is valid volume/chapter information.
|
||||
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial)
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret); // NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder
|
||||
}
|
||||
|
||||
// If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name
|
||||
if (Parser.HasSpecialMarker(fileName))
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ret.SpecialIndex = Parser.ParseSpecialIndex(fileName);
|
||||
ret.Chapters = Parser.DefaultChapter;
|
||||
ret.Volumes = Parser.LooseLeafVolume;
|
||||
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ret.Series))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic);
|
||||
}
|
||||
|
||||
// Pdfs may have .pdf in the series name, remove that
|
||||
if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf"))
|
||||
{
|
||||
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
|
||||
}
|
||||
|
||||
// v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number
|
||||
if (ret.IsSpecial)
|
||||
{
|
||||
ret.Volumes = $"{Parser.SpecialVolumeNumber}";
|
||||
}
|
||||
|
||||
return ret.Series == string.Empty ? null : ret;
|
||||
}
|
||||
|
||||
private ParserInfo ParseImage(string filePath, string rootPath, ParserInfo ret)
|
||||
{
|
||||
ret.Volumes = Parser.LooseLeafVolume;
|
||||
ret.Chapters = Parser.DefaultChapter;
|
||||
var directoryName = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
ret.Series = directoryName;
|
||||
|
||||
ParseFromFallbackFolders(filePath, rootPath, LibraryType.Image, ref ret);
|
||||
|
||||
|
||||
if (IsEmptyOrDefault(ret.Volumes, ret.Chapters))
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var parsedVolume = Parser.ParseVolume(ret.Filename);
|
||||
var parsedChapter = Parser.ParseChapter(ret.Filename);
|
||||
if (IsEmptyOrDefault(ret.Volumes, string.Empty) && !parsedVolume.Equals(Parser.LooseLeafVolume))
|
||||
{
|
||||
ret.Volumes = parsedVolume;
|
||||
}
|
||||
if (IsEmptyOrDefault(string.Empty, ret.Chapters) && !parsedChapter.Equals(Parser.DefaultChapter))
|
||||
{
|
||||
ret.Chapters = parsedChapter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Override the series name, as fallback folders needs it to try and parse folder name
|
||||
if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(directoryName, replaceSpecials: false);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool IsEmptyOrDefault(string volumes, string chapters)
|
||||
{
|
||||
return (string.IsNullOrEmpty(chapters) || chapters == Parser.DefaultChapter) &&
|
||||
(string.IsNullOrEmpty(volumes) || volumes == Parser.LooseLeafVolume);
|
||||
}
|
||||
public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null);
|
||||
|
||||
/// <summary>
|
||||
/// Fills out <see cref="ParserInfo"/> by trying to parse volume, chapters, and series from folders
|
||||
|
@ -176,13 +38,13 @@ public class DefaultParser : IDefaultParser
|
|||
/// <param name="ret">Expects a non-null ParserInfo which this method will populate</param>
|
||||
public void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret)
|
||||
{
|
||||
var fallbackFolders = _directoryService.GetFoldersTillRoot(rootPath, filePath)
|
||||
var fallbackFolders = directoryService.GetFoldersTillRoot(rootPath, filePath)
|
||||
.Where(f => !Parser.IsMangaSpecial(f))
|
||||
.ToList();
|
||||
|
||||
if (fallbackFolders.Count == 0)
|
||||
{
|
||||
var rootFolderName = _directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
var rootFolderName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
var series = Parser.ParseSeries(rootFolderName);
|
||||
|
||||
if (string.IsNullOrEmpty(series))
|
||||
|
@ -236,4 +98,12 @@ public class DefaultParser : IDefaultParser
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract bool IsApplicable(string filePath, LibraryType type);
|
||||
|
||||
protected static bool IsEmptyOrDefault(string volumes, string chapters)
|
||||
{
|
||||
return (string.IsNullOrEmpty(chapters) || chapters == Parser.DefaultChapter) &&
|
||||
(string.IsNullOrEmpty(volumes) || volumes == Parser.LooseLeafVolume);
|
||||
}
|
||||
}
|
||||
|
|
54
API/Services/Tasks/Scanner/Parser/ImageParser.cs
Normal file
54
API/Services/Tasks/Scanner/Parser/ImageParser.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using System.IO;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
namespace API.Services.Tasks.Scanner.Parser;
|
||||
#nullable enable
|
||||
|
||||
public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService)
|
||||
{
|
||||
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null)
|
||||
{
|
||||
if (type != LibraryType.Image || !Parser.IsImage(filePath)) return null;
|
||||
|
||||
var directoryName = directoryService.FileSystem.DirectoryInfo.New(rootPath).Name;
|
||||
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
||||
var ret = new ParserInfo
|
||||
{
|
||||
Series = directoryName,
|
||||
Volumes = Parser.LooseLeafVolume,
|
||||
Chapters = Parser.DefaultChapter,
|
||||
ComicInfo = comicInfo,
|
||||
Format = MangaFormat.Image,
|
||||
Filename = Path.GetFileName(filePath),
|
||||
FullFilePath = filePath,
|
||||
Title = fileName,
|
||||
};
|
||||
ParseFromFallbackFolders(filePath, libraryRoot, LibraryType.Image, ref ret);
|
||||
|
||||
if (IsEmptyOrDefault(ret.Volumes, ret.Chapters))
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ret.Volumes = $"{Parser.SpecialVolumeNumber}";
|
||||
}
|
||||
|
||||
// Override the series name, as fallback folders needs it to try and parse folder name
|
||||
if (string.IsNullOrEmpty(ret.Series) || ret.Series.Equals(directoryName))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(directoryName, replaceSpecials: false);
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(ret.Series) ? null : ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only applicable for Image files and Image library type
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public override bool IsApplicable(string filePath, LibraryType type)
|
||||
{
|
||||
return type == LibraryType.Image && Parser.IsImage(filePath);
|
||||
}
|
||||
}
|
|
@ -106,6 +106,12 @@ public static class Parser
|
|||
private static readonly Regex NormalizeRegex = new Regex(@"[^\p{L}0-9\+!]",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Supports Batman (2020) or Batman (2)
|
||||
/// </summary>
|
||||
private static readonly Regex SeriesAndYearRegex = new Regex(@"^\D+\s\((?<Year>\d+)\)$",
|
||||
MatchOptions, RegexTimeout);
|
||||
|
||||
/// <summary>
|
||||
/// Recognizes the Special token only
|
||||
/// </summary>
|
||||
|
@ -698,8 +704,9 @@ public static class Parser
|
|||
return MangaSpecialRegex.IsMatch(filePath);
|
||||
}
|
||||
|
||||
public static bool IsComicSpecial(string filePath)
|
||||
public static bool IsComicSpecial(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)) return false;
|
||||
filePath = ReplaceUnderscores(filePath);
|
||||
return ComicSpecialRegex.IsMatch(filePath);
|
||||
}
|
||||
|
@ -1125,13 +1132,32 @@ public static class Parser
|
|||
|
||||
// NOTE: This is failing for //localhost:5000/api/book/29919/book-resources?file=OPS/images/tick1.jpg
|
||||
var importFile = match.Groups["Filename"].Value;
|
||||
if (!importFile.Contains("?")) return importFile;
|
||||
if (!importFile.Contains('?')) return importFile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string RemoveExtensionIfSupported(string? filename)
|
||||
/// <summary>
|
||||
/// If the name matches exactly Series (Volume digits)
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsSeriesAndYear(string? name)
|
||||
{
|
||||
return !string.IsNullOrEmpty(name) && SeriesAndYearRegex.IsMatch(name);
|
||||
}
|
||||
|
||||
public static string ParseYear(string? name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name)) return string.Empty;
|
||||
var match = SeriesAndYearRegex.Match(name);
|
||||
if (!match.Success) return string.Empty;
|
||||
|
||||
return match.Groups["Year"].Value;
|
||||
}
|
||||
|
||||
public static string? RemoveExtensionIfSupported(string? filename)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filename)) return filename;
|
||||
|
||||
|
|
100
API/Services/Tasks/Scanner/Parser/PdfParser.cs
Normal file
100
API/Services/Tasks/Scanner/Parser/PdfParser.cs
Normal file
|
@ -0,0 +1,100 @@
|
|||
using System.IO;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
|
||||
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)
|
||||
{
|
||||
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
|
||||
var ret = new ParserInfo
|
||||
{
|
||||
Filename = Path.GetFileName(filePath),
|
||||
Format = Parser.ParseFormat(filePath),
|
||||
Title = Parser.RemoveExtensionIfSupported(fileName)!,
|
||||
FullFilePath = filePath,
|
||||
Series = string.Empty,
|
||||
ComicInfo = comicInfo,
|
||||
Chapters = type == LibraryType.Comic
|
||||
? Parser.ParseComicChapter(fileName)
|
||||
: Parser.ParseChapter(fileName)
|
||||
};
|
||||
|
||||
ret.Series = type == LibraryType.Comic ? Parser.ParseComicSeries(fileName) : Parser.ParseSeries(fileName);
|
||||
ret.Volumes = type == LibraryType.Comic ? Parser.ParseComicVolume(fileName) : Parser.ParseVolume(fileName);
|
||||
|
||||
if (ret.Series == string.Empty)
|
||||
{
|
||||
// Try to parse information out of each folder all the way to rootPath
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
var edition = Parser.ParseEdition(fileName);
|
||||
if (!string.IsNullOrEmpty(edition))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(ret.Series.Replace(edition, string.Empty), type is LibraryType.Comic);
|
||||
ret.Edition = edition;
|
||||
}
|
||||
|
||||
var isSpecial = type == LibraryType.Comic ? Parser.IsComicSpecial(fileName) : Parser.IsMangaSpecial(fileName);
|
||||
// We must ensure that we can only parse a special out. As some files will have v20 c171-180+Omake and that
|
||||
// could cause a problem as Omake is a special term, but there is valid volume/chapter information.
|
||||
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && isSpecial)
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
// NOTE: This can cause some complications, we should try to be a bit less aggressive to fallback to folder
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
// If we are a special with marker, we need to ensure we use the correct series name. we can do this by falling back to Folder name
|
||||
if (Parser.HasSpecialMarker(fileName))
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ret.SpecialIndex = Parser.ParseSpecialIndex(fileName);
|
||||
ret.Chapters = Parser.DefaultChapter;
|
||||
ret.Volumes = Parser.SpecialVolume;
|
||||
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && type == LibraryType.Book)
|
||||
{
|
||||
ret.IsSpecial = true;
|
||||
ret.Chapters = Parser.DefaultChapter;
|
||||
ret.Volumes = Parser.SpecialVolume;
|
||||
ParseFromFallbackFolders(filePath, rootPath, type, ref ret);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ret.Series))
|
||||
{
|
||||
ret.Series = Parser.CleanTitle(fileName, type is LibraryType.Comic);
|
||||
}
|
||||
|
||||
// Pdfs may have .pdf in the series name, remove that
|
||||
if (Parser.IsPdf(filePath) && ret.Series.ToLower().EndsWith(".pdf"))
|
||||
{
|
||||
ret.Series = ret.Series.Substring(0, ret.Series.Length - ".pdf".Length);
|
||||
}
|
||||
|
||||
// v0.8.x: Introducing a change where Specials will go in a separate Volume with a reserved number
|
||||
if (ret.IsSpecial)
|
||||
{
|
||||
ret.Volumes = $"{Parser.SpecialVolumeNumber}";
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(ret.Series) ? null : ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only applicable for PDF files
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public override bool IsApplicable(string filePath, LibraryType type)
|
||||
{
|
||||
return Parser.IsPdf(filePath);
|
||||
}
|
||||
}
|
|
@ -439,6 +439,14 @@ public class ProcessSeries : IProcessSeries
|
|||
}
|
||||
}
|
||||
|
||||
if (!series.Metadata.ImprintLocked)
|
||||
{
|
||||
foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Imprint))
|
||||
{
|
||||
PersonHelper.AddPersonIfNotExists(series.Metadata.People, person);
|
||||
}
|
||||
}
|
||||
|
||||
if (!series.Metadata.LettererLocked)
|
||||
{
|
||||
foreach (var person in chapter.People.Where(p => p.Role == PersonRole.Letterer))
|
||||
|
@ -496,6 +504,9 @@ public class ProcessSeries : IProcessSeries
|
|||
case PersonRole.Inker:
|
||||
if (!series.Metadata.InkerLocked) series.Metadata.People.Remove(person);
|
||||
break;
|
||||
case PersonRole.Imprint:
|
||||
if (!series.Metadata.ImprintLocked) series.Metadata.People.Remove(person);
|
||||
break;
|
||||
case PersonRole.Colorist:
|
||||
if (!series.Metadata.ColoristLocked) series.Metadata.People.Remove(person);
|
||||
break;
|
||||
|
@ -847,6 +858,10 @@ public class ProcessSeries : IProcessSeries
|
|||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Publisher);
|
||||
UpdatePeople(people, PersonRole.Publisher, AddPerson);
|
||||
|
||||
people = GetTagValues(comicInfo.Imprint);
|
||||
PersonHelper.RemovePeople(chapter.People, people, PersonRole.Imprint);
|
||||
UpdatePeople(people, PersonRole.Imprint, AddPerson);
|
||||
|
||||
var genres = GetTagValues(comicInfo.Genre);
|
||||
GenreHelper.KeepOnlySameGenreBetweenLists(chapter.Genres,
|
||||
genres.Select(g => new GenreBuilder(g).Build()).ToList());
|
||||
|
|
|
@ -5,7 +5,8 @@ export enum LibraryType {
|
|||
Comic = 1,
|
||||
Book = 2,
|
||||
Images = 3,
|
||||
LightNovel = 4
|
||||
LightNovel = 4,
|
||||
ComicVine = 5
|
||||
}
|
||||
|
||||
export interface Library {
|
||||
|
|
|
@ -29,6 +29,7 @@ export interface ChapterMetadata {
|
|||
characters: Array<Person>;
|
||||
pencillers: Array<Person>;
|
||||
inkers: Array<Person>;
|
||||
imprints: Array<Person>;
|
||||
colorists: Array<Person>;
|
||||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
export enum PersonRole {
|
||||
Other = 1,
|
||||
Artist = 2,
|
||||
Writer = 3,
|
||||
Penciller = 4,
|
||||
Inker = 5,
|
||||
Colorist = 6,
|
||||
Letterer = 7,
|
||||
CoverArtist = 8,
|
||||
Editor = 9,
|
||||
Publisher = 10,
|
||||
Character = 11,
|
||||
Translator = 12
|
||||
Other = 1,
|
||||
Artist = 2,
|
||||
Writer = 3,
|
||||
Penciller = 4,
|
||||
Inker = 5,
|
||||
Colorist = 6,
|
||||
Letterer = 7,
|
||||
CoverArtist = 8,
|
||||
Editor = 9,
|
||||
Publisher = 10,
|
||||
Character = 11,
|
||||
Translator = 12,
|
||||
Imprint = 13
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface SeriesMetadata {
|
|||
characters: Array<Person>;
|
||||
pencillers: Array<Person>;
|
||||
inkers: Array<Person>;
|
||||
imprints: Array<Person>;
|
||||
colorists: Array<Person>;
|
||||
letterers: Array<Person>;
|
||||
editors: Array<Person>;
|
||||
|
@ -40,6 +41,7 @@ export interface SeriesMetadata {
|
|||
characterLocked: boolean;
|
||||
pencillerLocked: boolean;
|
||||
inkerLocked: boolean;
|
||||
imprintLocked: boolean;
|
||||
coloristLocked: boolean;
|
||||
lettererLocked: boolean;
|
||||
editorLocked: boolean;
|
||||
|
|
|
@ -29,7 +29,8 @@ export enum FilterField
|
|||
FilePath = 25,
|
||||
WantToRead = 26,
|
||||
ReadingDate = 27,
|
||||
AverageRating = 28
|
||||
AverageRating = 28,
|
||||
Imprint = 29
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -15,4 +15,5 @@ export interface RelatedSeries {
|
|||
doujinshis: Array<Series>;
|
||||
parent: Array<Series>;
|
||||
editions: Array<Series>;
|
||||
annuals: Array<Series>;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ export enum RelationKind {
|
|||
* This is UI only. Backend will generate Parent series for everything but Prequel/Sequel
|
||||
*/
|
||||
Parent = 12,
|
||||
Edition = 13
|
||||
Edition = 13,
|
||||
Annual = 14
|
||||
}
|
||||
|
||||
const RelationKindsUnsorted = [
|
||||
|
@ -22,6 +23,7 @@ const RelationKindsUnsorted = [
|
|||
{text: 'Sequel', value: RelationKind.Sequel},
|
||||
{text: 'Spin Off', value: RelationKind.SpinOff},
|
||||
{text: 'Adaptation', value: RelationKind.Adaptation},
|
||||
{text: 'Annual', value: RelationKind.Annual},
|
||||
{text: 'Alternative Setting', value: RelationKind.AlternativeSetting},
|
||||
{text: 'Alternative Version', value: RelationKind.AlternativeVersion},
|
||||
{text: 'Side Story', value: RelationKind.SideStory},
|
||||
|
|
|
@ -28,6 +28,8 @@ export class FilterFieldPipe implements PipeTransform {
|
|||
return translate('filter-field-pipe.genres');
|
||||
case FilterField.Inker:
|
||||
return translate('filter-field-pipe.inker');
|
||||
case FilterField.Imprint:
|
||||
return translate('filter-field-pipe.imprint');
|
||||
case FilterField.Languages:
|
||||
return translate('filter-field-pipe.languages');
|
||||
case FilterField.Libraries:
|
||||
|
|
|
@ -29,6 +29,8 @@ export class PersonRolePipe implements PipeTransform {
|
|||
return this.translocoService.translate('person-role-pipe.penciller');
|
||||
case PersonRole.Publisher:
|
||||
return this.translocoService.translate('person-role-pipe.publisher');
|
||||
case PersonRole.Imprint:
|
||||
return this.translocoService.translate('person-role-pipe.imprint');
|
||||
case PersonRole.Writer:
|
||||
return this.translocoService.translate('person-role-pipe.writer');
|
||||
case PersonRole.Other:
|
||||
|
|
|
@ -39,6 +39,8 @@ export class RelationshipPipe implements PipeTransform {
|
|||
return this.translocoService.translate('relationship-pipe.parent');
|
||||
case RelationKind.Edition:
|
||||
return this.translocoService.translate('relationship-pipe.edition');
|
||||
case RelationKind.Annual:
|
||||
return this.translocoService.translate('relationship-pipe.annual');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -199,10 +199,11 @@ export class SeriesService {
|
|||
updateRelationships(seriesId: number, adaptations: Array<number>, characters: Array<number>,
|
||||
contains: Array<number>, others: Array<number>, prequels: Array<number>,
|
||||
sequels: Array<number>, sideStories: Array<number>, spinOffs: Array<number>,
|
||||
alternativeSettings: Array<number>, alternativeVersions: Array<number>, doujinshis: Array<number>, editions: Array<number>) {
|
||||
alternativeSettings: Array<number>, alternativeVersions: Array<number>,
|
||||
doujinshis: Array<number>, editions: Array<number>, annuals: Array<number>) {
|
||||
return this.httpClient.post(this.baseUrl + 'series/update-related?seriesId=' + seriesId,
|
||||
{seriesId, adaptations, characters, sequels, prequels, contains, others, sideStories, spinOffs,
|
||||
alternativeSettings, alternativeVersions, doujinshis, editions});
|
||||
alternativeSettings, alternativeVersions, doujinshis, editions, annuals});
|
||||
}
|
||||
|
||||
getSeriesDetail(seriesId: number) {
|
||||
|
|
|
@ -229,6 +229,23 @@
|
|||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="imprint" class="form-label">{{t('imprint-label')}}</label>
|
||||
<app-typeahead (selectedData)="updatePerson($event, PersonRole.Imprint);metadata.publisherLocked = true" [settings]="getPersonsSettings(PersonRole.Imprint)"
|
||||
[(locked)]="metadata.imprintLocked" (onUnlock)="metadata.imprintLocked = false"
|
||||
(newItemAdded)="metadata.imprintLocked = true">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="mb-3">
|
||||
<label for="penciller" class="form-label">{{t('penciller-label')}}</label>
|
||||
|
|
|
@ -487,6 +487,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
this.updateFromPreset('letterer', this.metadata.letterers, PersonRole.Letterer),
|
||||
this.updateFromPreset('penciller', this.metadata.pencillers, PersonRole.Penciller),
|
||||
this.updateFromPreset('publisher', this.metadata.publishers, PersonRole.Publisher),
|
||||
this.updateFromPreset('imprint', this.metadata.imprints, PersonRole.Imprint),
|
||||
this.updateFromPreset('translator', this.metadata.translators, PersonRole.Translator)
|
||||
]).pipe(map(results => {
|
||||
return of(true);
|
||||
|
@ -634,6 +635,9 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
case PersonRole.Publisher:
|
||||
this.metadata.publishers = persons;
|
||||
break;
|
||||
case PersonRole.Imprint:
|
||||
this.metadata.imprints = persons;
|
||||
break;
|
||||
case PersonRole.Writer:
|
||||
this.metadata.writers = persons;
|
||||
break;
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
&& chapter.pencillers.length === 0 && chapter.inkers.length === 0
|
||||
&& chapter.colorists.length === 0 && chapter.letterers.length === 0
|
||||
&& chapter.editors.length === 0 && chapter.publishers.length === 0
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0">
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0
|
||||
&& chapter.imprints.length === 0">
|
||||
{{t('no-data')}}
|
||||
</span>
|
||||
<div class="row g-0">
|
||||
<div class="container-flex row row-cols-auto row-cols-lg-5 g-2 g-lg-3 me-0 mt-2">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>{{t('writers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
|
@ -81,6 +82,15 @@
|
|||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.imprints && chapter.imprints.length > 0">
|
||||
<h6>{{t('imprints-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.imprints">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>{{t('characters-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
|
|
|
@ -71,7 +71,7 @@ export class EditSeriesRelationComponent implements OnInit {
|
|||
focusTypeahead = new EventEmitter();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seriesService.getRelatedForSeries(this.series.id).subscribe(async relations => {
|
||||
this.seriesService.getRelatedForSeries(this.series.id).subscribe( relations => {
|
||||
this.setupRelationRows(relations.prequels, RelationKind.Prequel);
|
||||
this.setupRelationRows(relations.sequels, RelationKind.Sequel);
|
||||
this.setupRelationRows(relations.sideStories, RelationKind.SideStory);
|
||||
|
@ -85,6 +85,7 @@ export class EditSeriesRelationComponent implements OnInit {
|
|||
this.setupRelationRows(relations.contains, RelationKind.Contains);
|
||||
this.setupRelationRows(relations.parent, RelationKind.Parent);
|
||||
this.setupRelationRows(relations.editions, RelationKind.Edition);
|
||||
this.setupRelationRows(relations.annuals, RelationKind.Annual);
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
|
||||
|
@ -181,9 +182,10 @@ export class EditSeriesRelationComponent implements OnInit {
|
|||
const alternativeVersions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeVersion && item.series !== undefined).map(item => item.series!.id);
|
||||
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);
|
||||
const editions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Edition && item.series !== undefined).map(item => item.series!.id);
|
||||
const annuals = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Annual && item.series !== undefined).map(item => item.series!.id);
|
||||
|
||||
// NOTE: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
|
||||
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis, editions).subscribe(() => {});
|
||||
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis, editions, annuals).subscribe(() => {});
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,20 @@
|
|||
{{Number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="LibraryType.ComicVine">
|
||||
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullComicTitle">
|
||||
{{titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullComicTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle !== ''">
|
||||
{{Number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
{{Number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + Number : volumeTitle) : t('special')}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="LibraryType.Manga">
|
||||
<ng-container *ngIf="titleName !== '' && prioritizeTitleName; else fullMangaTitle">
|
||||
{{titleName}}
|
||||
|
@ -30,5 +44,8 @@
|
|||
<ng-container *ngSwitchCase="LibraryType.LightNovel">
|
||||
{{volumeTitle}}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Images">
|
||||
{{Number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + Number : volumeTitle) : t('special')}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
@ -64,7 +64,8 @@ const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, Fi
|
|||
FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer,
|
||||
FilterField.Colorist, FilterField.Inker, FilterField.Penciller,
|
||||
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
|
||||
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags
|
||||
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags,
|
||||
FilterField.Imprint
|
||||
];
|
||||
const BooleanFields = [FilterField.WantToRead];
|
||||
const DateFields = [FilterField.ReadingDate];
|
||||
|
@ -297,6 +298,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||
case FilterField.Letterer: return this.getPersonOptions(PersonRole.Letterer);
|
||||
case FilterField.Penciller: return this.getPersonOptions(PersonRole.Penciller);
|
||||
case FilterField.Publisher: return this.getPersonOptions(PersonRole.Publisher);
|
||||
case FilterField.Imprint: return this.getPersonOptions(PersonRole.Imprint);
|
||||
case FilterField.Translators: return this.getPersonOptions(PersonRole.Translator);
|
||||
case FilterField.Writers: return this.getPersonOptions(PersonRole.Writer);
|
||||
}
|
||||
|
|
|
@ -178,6 +178,9 @@ export class NavHeaderComponent implements OnInit {
|
|||
case PersonRole.Publisher:
|
||||
this.goTo({field: FilterField.Publisher, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Imprint:
|
||||
this.goTo({field: FilterField.Imprint, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
case PersonRole.Translator:
|
||||
this.goTo({field: FilterField.Translators, comparison: FilterComparison.Equal, value: filter});
|
||||
break;
|
||||
|
|
|
@ -55,7 +55,7 @@ import {
|
|||
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
|
||||
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service';
|
||||
import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Chapter, SpecialVolumeNumber} from 'src/app/_models/chapter';
|
||||
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
|
||||
import {Device} from 'src/app/_models/device/device';
|
||||
import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event';
|
||||
import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event';
|
||||
|
@ -67,7 +67,6 @@ import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
|
|||
import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata';
|
||||
import {User} from 'src/app/_models/user';
|
||||
import {Volume} from 'src/app/_models/volume';
|
||||
import {LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
|
||||
import {ActionService} from 'src/app/_services/action.service';
|
||||
|
@ -339,12 +338,20 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
|
||||
get ShowStorylineTab() {
|
||||
return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel) && (this.volumes.length > 0 || this.chapters.length > 0);
|
||||
if (this.libraryType === LibraryType.ComicVine) return false;
|
||||
return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel && this.libraryType !== LibraryType.Comic)
|
||||
&& (this.volumes.length > 0 || this.chapters.length > 0);
|
||||
}
|
||||
|
||||
get ShowVolumeTab() {
|
||||
if (this.libraryType === LibraryType.ComicVine) {
|
||||
if (this.volumes.length > 1) return true;
|
||||
if (this.specials.length === 0 && this.chapters.length === 0) return true;
|
||||
return false;
|
||||
}
|
||||
return this.volumes.length > 0;
|
||||
}
|
||||
|
||||
get ShowChaptersTab() {
|
||||
return this.chapters.length > 0;
|
||||
}
|
||||
|
@ -662,6 +669,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
...relations.doujinshis.map(item => this.createRelatedSeries(item, RelationKind.Doujinshi)),
|
||||
...relations.parent.map(item => this.createRelatedSeries(item, RelationKind.Parent)),
|
||||
...relations.editions.map(item => this.createRelatedSeries(item, RelationKind.Edition)),
|
||||
...relations.annuals.map(item => this.createRelatedSeries(item, RelationKind.Annual)),
|
||||
];
|
||||
if (this.relations.length > 0) {
|
||||
this.hasRelations = true;
|
||||
|
@ -730,7 +738,16 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
if (this.volumes.length === 0 && this.chapters.length === 0 && this.specials.length > 0) {
|
||||
this.activeTabId = TabID.Specials;
|
||||
} else {
|
||||
this.activeTabId = TabID.Storyline;
|
||||
if (this.libraryType == LibraryType.Comic || this.libraryType == LibraryType.ComicVine) {
|
||||
if (this.chapters.length === 0) {
|
||||
this.activeTabId = TabID.Specials;
|
||||
} else {
|
||||
this.activeTabId = TabID.Chapters;
|
||||
}
|
||||
} else {
|
||||
this.activeTabId = TabID.Storyline;
|
||||
}
|
||||
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
|
|
@ -154,6 +154,12 @@
|
|||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.imprints" [libraryId]="series.libraryId" [queryParam]="FilterField.Imprint" [heading]="t('imprints-title')">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnChanges, OnInit,
|
||||
SimpleChanges,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
|
@ -46,7 +46,7 @@ import {Rating} from "../../../_models/rating";
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SeriesMetadataDetailComponent implements OnChanges {
|
||||
export class SeriesMetadataDetailComponent implements OnChanges, OnInit {
|
||||
|
||||
protected readonly imageService = inject(ImageService);
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
|
@ -83,13 +83,26 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||
return this.seriesMetadata?.webLinks.split(',') || [];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
ngOnInit() {
|
||||
// If on desktop, we can just have all the data expanded by default:
|
||||
this.isCollapsed = this.utilityService.getActiveBreakpoint() < Breakpoint.Desktop;
|
||||
// Check if there is a lot of extended data, if so, re-collapse
|
||||
const sum = (this.seriesMetadata.colorists.length + this.seriesMetadata.editors.length
|
||||
+ this.seriesMetadata.coverArtists.length + this.seriesMetadata.inkers.length
|
||||
+ this.seriesMetadata.letterers.length + this.seriesMetadata.pencillers.length
|
||||
+ this.seriesMetadata.publishers.length + this.seriesMetadata.characters.length
|
||||
+ this.seriesMetadata.imprints.length + this.seriesMetadata.translators.length
|
||||
+ this.seriesMetadata.writers.length) / 11;
|
||||
if (sum > 10) {
|
||||
this.isCollapsed = true;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
|
||||
|
||||
|
||||
this.hasExtendedProperties = this.seriesMetadata.colorists.length > 0 ||
|
||||
this.seriesMetadata.editors.length > 0 ||
|
||||
this.seriesMetadata.coverArtists.length > 0 ||
|
||||
|
@ -98,6 +111,7 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||
this.seriesMetadata.pencillers.length > 0 ||
|
||||
this.seriesMetadata.publishers.length > 0 ||
|
||||
this.seriesMetadata.characters.length > 0 ||
|
||||
this.seriesMetadata.imprints.length > 0 ||
|
||||
this.seriesMetadata.translators.length > 0;
|
||||
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ export class UtilityService {
|
|||
case LibraryType.LightNovel:
|
||||
return this.translocoService.translate('common.book-num') + (includeSpace ? ' ' : '');
|
||||
case LibraryType.Comic:
|
||||
case LibraryType.ComicVine:
|
||||
if (includeHash) {
|
||||
return this.translocoService.translate('common.issue-hash-num');
|
||||
}
|
||||
|
|
|
@ -188,6 +188,7 @@ export class SideNavComponent implements OnInit {
|
|||
case LibraryType.LightNovel:
|
||||
return 'fa-book';
|
||||
case LibraryType.Comic:
|
||||
case LibraryType.ComicVine:
|
||||
case LibraryType.Manga:
|
||||
return 'fa-book-open';
|
||||
case LibraryType.Images:
|
||||
|
|
|
@ -459,7 +459,8 @@
|
|||
"side-story": "Side Story",
|
||||
"spin-off": "Spin Off",
|
||||
"parent": "Parent",
|
||||
"edition": "Edition"
|
||||
"edition": "Edition",
|
||||
"annual": "Annual"
|
||||
},
|
||||
|
||||
"publication-status-pipe": {
|
||||
|
@ -481,7 +482,8 @@
|
|||
"penciller": "Penciller",
|
||||
"publisher": "Publisher",
|
||||
"writer": "Writer",
|
||||
"other": "Other"
|
||||
"other": "Other",
|
||||
"imprint": "Imprint"
|
||||
},
|
||||
|
||||
"manga-format-pipe": {
|
||||
|
@ -755,6 +757,7 @@
|
|||
"translators-title": "Translators",
|
||||
"pencillers-title": "Pencillers",
|
||||
"publishers-title": "Publishers",
|
||||
"imprints-title": "Imprints",
|
||||
|
||||
"promoted": "{{common.promoted}}",
|
||||
"see-more": "See More",
|
||||
|
@ -930,6 +933,7 @@
|
|||
"writers-title": "{{series-metadata-detail.writers-title}}",
|
||||
"genres-title": "{{series-metadata-detail.genres-title}}",
|
||||
"publishers-title": "{{series-metadata-detail.publishers-title}}",
|
||||
"imprints-title": "{{series-metadata-detail.imprints-title}}",
|
||||
"tags-title": "{{series-metadata-detail.tags-title}}",
|
||||
"not-defined": "Not defined",
|
||||
"read": "{{common.read}}",
|
||||
|
@ -1683,6 +1687,7 @@
|
|||
"cover-artist-label": "Cover Artist",
|
||||
"writer-label": "Writer",
|
||||
"publisher-label": "Publisher",
|
||||
"imprint-label": "Imprint",
|
||||
"penciller-label": "Penciller",
|
||||
"letterer-label": "Letterer",
|
||||
"inker-label": "Inker",
|
||||
|
@ -1944,6 +1949,7 @@
|
|||
"letterer": "Letterer",
|
||||
"publication-status": "Publication Status",
|
||||
"penciller": "Penciller",
|
||||
"imprint": "Imprint",
|
||||
"publisher": "Publisher",
|
||||
"read-progress": "Read Progress",
|
||||
"read-time": "Read Time",
|
||||
|
|
88
openapi.json
88
openapi.json
|
@ -7,7 +7,7 @@
|
|||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.14.5"
|
||||
"version": "0.7.14.6"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
@ -2909,7 +2909,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -2922,7 +2923,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -2935,7 +2937,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -3200,7 +3203,8 @@
|
|||
9,
|
||||
10,
|
||||
11,
|
||||
12
|
||||
12,
|
||||
13
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -3619,7 +3623,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -9191,7 +9196,8 @@
|
|||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
13,
|
||||
14
|
||||
],
|
||||
"type": "integer",
|
||||
"description": "Represents a relationship between Series",
|
||||
|
@ -13526,7 +13532,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -14150,7 +14157,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"description": "Library type",
|
||||
|
@ -14273,6 +14281,13 @@
|
|||
},
|
||||
"nullable": true
|
||||
},
|
||||
"imprints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"colorists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -15599,7 +15614,8 @@
|
|||
25,
|
||||
26,
|
||||
27,
|
||||
28
|
||||
28,
|
||||
29
|
||||
],
|
||||
"type": "integer",
|
||||
"description": "Represents the field which will dictate the value type and the Extension used for filtering",
|
||||
|
@ -15916,7 +15932,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -16030,7 +16047,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -16529,7 +16547,8 @@
|
|||
9,
|
||||
10,
|
||||
11,
|
||||
12
|
||||
12,
|
||||
13
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -16574,7 +16593,8 @@
|
|||
9,
|
||||
10,
|
||||
11,
|
||||
12
|
||||
12,
|
||||
13
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -17025,7 +17045,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -17078,7 +17099,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -17286,6 +17308,13 @@
|
|||
"$ref": "#/components/schemas/SeriesDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"annuals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SeriesDto"
|
||||
},
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
@ -18217,6 +18246,9 @@
|
|||
"inkerLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"imprintLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lettererLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -18327,6 +18359,13 @@
|
|||
},
|
||||
"nullable": true
|
||||
},
|
||||
"imprints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonDto"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"colorists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
@ -18450,6 +18489,9 @@
|
|||
"inkerLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"imprintLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lettererLocked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -18496,7 +18538,8 @@
|
|||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
13,
|
||||
14
|
||||
],
|
||||
"type": "integer",
|
||||
"description": "Represents a relationship between Series",
|
||||
|
@ -19417,7 +19460,8 @@
|
|||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
4,
|
||||
5
|
||||
],
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
|
@ -19841,6 +19885,14 @@
|
|||
"format": "int32"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"annuals": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue