Added a new library setting to disable metadata completely.

This commit is contained in:
Joseph Milazzo 2025-06-21 09:34:51 -05:00
parent 3a01e9af3a
commit 52f6e235d0
33 changed files with 4026 additions and 72 deletions

View file

@ -36,7 +36,7 @@ public class ComicVineParserTests
public void Parse_SeriesWithComicInfo() 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)/", 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() RootDirectory, LibraryType.ComicVine, true, new ComicInfo()
{ {
Series = "Birds of Prey", Series = "Birds of Prey",
Volume = "2002" Volume = "2002"
@ -54,7 +54,7 @@ public class ComicVineParserTests
public void Parse_SeriesWithDirectoryNameAsSeriesYear() 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)/", 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); RootDirectory, LibraryType.ComicVine, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("Birds of Prey (2002)", actual.Series); Assert.Equal("Birds of Prey (2002)", actual.Series);
@ -69,7 +69,7 @@ public class ComicVineParserTests
public void Parse_SeriesWithADirectoryNameAsSeriesYear() 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/", 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); RootDirectory, LibraryType.ComicVine, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("Birds of Prey (1999)", actual.Series); Assert.Equal("Birds of Prey (1999)", actual.Series);
@ -84,7 +84,7 @@ public class ComicVineParserTests
public void Parse_FallbackToDirectoryNameOnly() public void Parse_FallbackToDirectoryNameOnly()
{ {
var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/", var actual = _parser.Parse("C:/Comics/DC Comics/Blood Syndicate/Blood Syndicate 001 (1999).cbz", "C:/Comics/DC Comics/",
RootDirectory, LibraryType.ComicVine, null); RootDirectory, LibraryType.ComicVine, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("Blood Syndicate", actual.Series); Assert.Equal("Blood Syndicate", actual.Series);

View file

@ -33,7 +33,7 @@ public class DefaultParserTests
[InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")] [InlineData("C:/", "C:/Something Random/Mujaki no Rakuen SP01.cbz", "Something Random")]
public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries) public void ParseFromFallbackFolders_FallbackShouldParseSeries(string rootDir, string inputPath, string expectedSeries)
{ {
var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, null); var actual = _defaultParser.Parse(inputPath, rootDir, rootDir, LibraryType.Manga, true, null);
if (actual == null) if (actual == null)
{ {
Assert.NotNull(actual); Assert.NotNull(actual);
@ -74,7 +74,7 @@ public class DefaultParserTests
fs.AddFile(inputFile, new MockFileData("")); fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs); var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new BasicParser(ds, new ImageParser(ds)); var parser = new BasicParser(ds, new ImageParser(ds));
var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null); var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series); Assert.Equal(expectedParseInfo, actual.Series);
} }
@ -90,7 +90,7 @@ public class DefaultParserTests
fs.AddFile(inputFile, new MockFileData("")); fs.AddFile(inputFile, new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs); var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fs);
var parser = new BasicParser(ds, new ImageParser(ds)); var parser = new BasicParser(ds, new ImageParser(ds));
var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, null); var actual = parser.Parse(inputFile, rootDirectory, rootDirectory, LibraryType.Manga, true, null);
_defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual); _defaultParser.ParseFromFallbackFolders(inputFile, rootDirectory, LibraryType.Manga, ref actual);
Assert.Equal(expectedParseInfo, actual.Series); Assert.Equal(expectedParseInfo, actual.Series);
} }
@ -251,7 +251,7 @@ public class DefaultParserTests
foreach (var file in expected.Keys) foreach (var file in expected.Keys)
{ {
var expectedInfo = expected[file]; var expectedInfo = expected[file];
var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, null); var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Manga, true, null);
if (expectedInfo == null) if (expectedInfo == null)
{ {
Assert.Null(actual); Assert.Null(actual);
@ -289,7 +289,7 @@ public class DefaultParserTests
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, null); var actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Monster #8", "E:/Manga", LibraryType.Manga, true, null);
Assert.NotNull(actual2); Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format); Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -315,7 +315,7 @@ public class DefaultParserTests
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, null); actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga",LibraryType.Manga, true, null);
Assert.NotNull(actual2); Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format); Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -341,7 +341,7 @@ public class DefaultParserTests
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, null); actual2 = _defaultParser.Parse(filepath, @"E:/Manga/Extra layer for no reason/", "E:/Manga", LibraryType.Manga, true, null);
Assert.NotNull(actual2); Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format); Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -383,7 +383,7 @@ public class DefaultParserTests
FullFilePath = filepath FullFilePath = filepath
}; };
var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null); var actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
@ -412,7 +412,7 @@ public class DefaultParserTests
FullFilePath = filepath FullFilePath = filepath
}; };
actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, null); actual = parser.Parse(filepath, rootPath, rootPath, LibraryType.Manga, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expected.Format, actual.Format); Assert.Equal(expected.Format, actual.Format);
@ -475,7 +475,7 @@ public class DefaultParserTests
foreach (var file in expected.Keys) foreach (var file in expected.Keys)
{ {
var expectedInfo = expected[file]; var expectedInfo = expected[file];
var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, null); var actual = _defaultParser.Parse(file, rootPath, rootPath, LibraryType.Comic, true, null);
if (expectedInfo == null) if (expectedInfo == null)
{ {
Assert.Null(actual); Assert.Null(actual);

View file

@ -34,7 +34,7 @@ public class ImageParserTests
public void Parse_SeriesWithDirectoryName() public void Parse_SeriesWithDirectoryName()
{ {
var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/", var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01/01.jpg", "C:/Comics/Birds of Prey/",
RootDirectory, LibraryType.Image, null); RootDirectory, LibraryType.Image, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("Birds of Prey", actual.Series); Assert.Equal("Birds of Prey", actual.Series);
@ -48,7 +48,7 @@ public class ImageParserTests
public void Parse_SeriesWithNoNestedChapter() public void Parse_SeriesWithNoNestedChapter()
{ {
var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/", var actual = _parser.Parse("C:/Comics/Birds of Prey/Chapter 01 page 01.jpg", "C:/Comics/",
RootDirectory, LibraryType.Image, null); RootDirectory, LibraryType.Image, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("Birds of Prey", actual.Series); Assert.Equal("Birds of Prey", actual.Series);
@ -62,7 +62,7 @@ public class ImageParserTests
public void Parse_SeriesWithLooseImages() public void Parse_SeriesWithLooseImages()
{ {
var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/", var actual = _parser.Parse("C:/Comics/Birds of Prey/page 01.jpg", "C:/Comics/",
RootDirectory, LibraryType.Image, null); RootDirectory, LibraryType.Image, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("Birds of Prey", actual.Series); Assert.Equal("Birds of Prey", actual.Series);

View file

@ -35,7 +35,7 @@ public class PdfParserTests
{ {
var actual = _parser.Parse("C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/A Dictionary of Japanese Food - Ingredients and Culture.pdf", 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/", "C:/Books/A Dictionary of Japanese Food - Ingredients and Culture/",
RootDirectory, LibraryType.Book, null); RootDirectory, LibraryType.Book, true, null);
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series); Assert.Equal("A Dictionary of Japanese Food - Ingredients and Culture", actual.Series);

View file

@ -34,7 +34,7 @@ public class ImageParsingTests
Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image, Chapters = "8", Filename = "13.jpg", Format = MangaFormat.Image,
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, null); var actual2 = _parser.Parse(filepath, @"E:\Manga\Monster #8", "E:/Manga", LibraryType.Image, true, null);
Assert.NotNull(actual2); Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format); Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -60,7 +60,7 @@ public class ImageParsingTests
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null); actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null);
Assert.NotNull(actual2); Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format); Assert.Equal(expectedInfo2.Format, actual2.Format);
@ -86,7 +86,7 @@ public class ImageParsingTests
FullFilePath = filepath, IsSpecial = false FullFilePath = filepath, IsSpecial = false
}; };
actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, null); actual2 = _parser.Parse(filepath, @"E:\Manga\Extra layer for no reason\", "E:/Manga", LibraryType.Image, true, null);
Assert.NotNull(actual2); Assert.NotNull(actual2);
_testOutputHelper.WriteLine($"Validating {filepath}"); _testOutputHelper.WriteLine($"Validating {filepath}");
Assert.Equal(expectedInfo2.Format, actual2.Format); Assert.Equal(expectedInfo2.Format, actual2.Format);

View file

@ -137,7 +137,7 @@ public class BookServiceTests
var comicInfo = _bookService.GetComicInfo(filePath); var comicInfo = _bookService.GetComicInfo(filePath);
Assert.NotNull(comicInfo); Assert.NotNull(comicInfo);
var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, comicInfo); var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, true, comicInfo);
Assert.NotNull(parserInfo); Assert.NotNull(parserInfo);
Assert.Equal(parserInfo.Title, comicInfo.Title); Assert.Equal(parserInfo.Title, comicInfo.Title);
Assert.Equal(parserInfo.Series, comicInfo.Title); Assert.Equal(parserInfo.Series, comicInfo.Title);

View file

@ -50,12 +50,12 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type) public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }

View file

@ -42,7 +42,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest
_externalMetadataService = new ExternalMetadataService(UnitOfWork, Substitute.For<ILogger<ExternalMetadataService>>(), _externalMetadataService = new ExternalMetadataService(UnitOfWork, Substitute.For<ILogger<ExternalMetadataService>>(),
Mapper, Substitute.For<ILicenseService>(), Substitute.For<IScrobblingService>(), Substitute.For<IEventHub>(), Mapper, Substitute.For<ILicenseService>(), Substitute.For<IScrobblingService>(), Substitute.For<IEventHub>(),
Substitute.For<ICoverDbService>()); Substitute.For<ICoverDbService>(), Substitute.For<IKavitaPlusApiService>());
} }
#region Gloabl #region Gloabl

View file

@ -58,35 +58,35 @@ public class MockReadingItemService : IReadingItemService
throw new NotImplementedException(); throw new NotImplementedException();
} }
public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type) public ParserInfo Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata)
{ {
if (_comicVineParser.IsApplicable(path, type)) if (_comicVineParser.IsApplicable(path, type))
{ {
return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_imageParser.IsApplicable(path, type)) if (_imageParser.IsApplicable(path, type))
{ {
return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_bookParser.IsApplicable(path, type)) if (_bookParser.IsApplicable(path, type))
{ {
return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_pdfParser.IsApplicable(path, type)) if (_pdfParser.IsApplicable(path, type))
{ {
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_basicParser.IsApplicable(path, type)) if (_basicParser.IsApplicable(path, type))
{ {
return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
return null; return null;
} }
public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) public ParserInfo ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata)
{ {
return Parse(path, rootPath, libraryRoot, type); return Parse(path, rootPath, libraryRoot, type, enableMetadata);
} }
} }

View file

@ -938,4 +938,37 @@ public class ScannerServiceTests : AbstractDbTest
Assert.True(sortedChapters[1].SortOrder.Is(4f)); Assert.True(sortedChapters[1].SortOrder.Is(4f));
Assert.True(sortedChapters[2].SortOrder.Is(5f)); Assert.True(sortedChapters[2].SortOrder.Is(5f));
} }
[Fact]
public async Task ScanLibrary_MetadataEnabled_NoOverrides()
{
const string testcase = "Series with Localized 2 - Manga.json";
// Get the first file and generate a ComicInfo
var infos = new Dictionary<string, ComicInfo>();
infos.Add("Immoral Guild v01.cbz", new ComicInfo()
{
Series = "Immoral Guild",
LocalizedSeries = "Futoku no Guild" // Filename has a capital N and localizedSeries has lowercase
});
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
// Disable metadata
library.EnableMetadata = false;
UnitOfWork.LibraryRepository.Update(library);
await UnitOfWork.CommitAsync();
var scanner = _scannerHelper.CreateServices();
await scanner.ScanLibrary(library.Id);
var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
// Validate that there are 2 series
Assert.NotNull(postLib);
Assert.Equal(2, postLib.Series.Count);
Assert.Contains(postLib.Series, x => x.Name == "Immoral Guild");
Assert.Contains(postLib.Series, x => x.Name == "Futoku No Guild");
}
} }

View file

@ -623,6 +623,7 @@ public class LibraryController : BaseApiController
library.ManageReadingLists = dto.ManageReadingLists; library.ManageReadingLists = dto.ManageReadingLists;
library.AllowScrobbling = dto.AllowScrobbling; library.AllowScrobbling = dto.AllowScrobbling;
library.AllowMetadataMatching = dto.AllowMetadataMatching; library.AllowMetadataMatching = dto.AllowMetadataMatching;
library.EnableMetadata = dto.EnableMetadata;
library.LibraryFileTypes = dto.FileGroupTypes library.LibraryFileTypes = dto.FileGroupTypes
.Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id})
.Distinct() .Distinct()

View file

@ -65,5 +65,5 @@ public sealed record LibraryDto
/// </summary> /// </summary>
/// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks> /// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks>
/// <remarks>Requires a valid LicenseKey</remarks> /// <remarks>Requires a valid LicenseKey</remarks>
public bool AllowMetadataMatching { get; set; } = true; public bool EnableMetadata { get; set; } = true;
} }

View file

@ -28,6 +28,8 @@ public sealed record UpdateLibraryDto
public bool AllowScrobbling { get; init; } public bool AllowScrobbling { get; init; }
[Required] [Required]
public bool AllowMetadataMatching { get; init; } public bool AllowMetadataMatching { get; init; }
[Required]
public bool EnableMetadata { get; init; }
/// <summary> /// <summary>
/// What types of files to allow the scanner to pickup /// What types of files to allow the scanner to pickup
/// </summary> /// </summary>

View file

@ -147,6 +147,9 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
builder.Entity<Library>() builder.Entity<Library>()
.Property(b => b.AllowMetadataMatching) .Property(b => b.AllowMetadataMatching)
.HasDefaultValue(true); .HasDefaultValue(true);
builder.Entity<Library>()
.Property(b => b.EnableMetadata)
.HasDefaultValue(true);
builder.Entity<Chapter>() builder.Entity<Chapter>()
.Property(b => b.WebLinks) .Property(b => b.WebLinks)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
/// <inheritdoc />
public partial class EnableMetadataLibrary : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "EnableMetadata",
table: "Library",
type: "INTEGER",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EnableMetadata",
table: "Library");
}
}
}

View file

@ -15,7 +15,7 @@ namespace API.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
modelBuilder.Entity("API.Entities.AppRole", b => modelBuilder.Entity("API.Entities.AppRole", b =>
{ {
@ -1296,6 +1296,11 @@ namespace API.Data.Migrations
b.Property<DateTime>("CreatedUtc") b.Property<DateTime>("CreatedUtc")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<bool>("EnableMetadata")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("FolderWatching") b.Property<bool>("FolderWatching")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View file

@ -48,6 +48,10 @@ public class Library : IEntityDate, IHasCoverImage
/// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks> /// <remarks>This does not exclude the library from being linked to wrt Series Relationships</remarks>
/// <remarks>Requires a valid LicenseKey</remarks> /// <remarks>Requires a valid LicenseKey</remarks>
public bool AllowMetadataMatching { get; set; } = true; public bool AllowMetadataMatching { get; set; } = true;
/// <summary>
/// Should Kavita read metadata files from the library
/// </summary>
public bool EnableMetadata { get; set; } = true;
public DateTime Created { get; set; } public DateTime Created { get; set; }

View file

@ -110,6 +110,12 @@ public class LibraryBuilder : IEntityBuilder<Library>
return this; return this;
} }
public LibraryBuilder WithEnableMetadata(bool enable)
{
_library.EnableMetadata = enable;
return this;
}
public LibraryBuilder WithAllowScrobbling(bool allowScrobbling) public LibraryBuilder WithAllowScrobbling(bool allowScrobbling)
{ {
_library.AllowScrobbling = allowScrobbling; _library.AllowScrobbling = allowScrobbling;

View file

@ -12,7 +12,7 @@ public interface IReadingItemService
int GetNumberOfPages(string filePath, MangaFormat format); int GetNumberOfPages(string filePath, MangaFormat format);
string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default); 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); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type); ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata);
} }
public class ReadingItemService : IReadingItemService public class ReadingItemService : IReadingItemService
@ -71,11 +71,12 @@ public class ReadingItemService : IReadingItemService
/// <param name="path">Path of a file</param> /// <param name="path">Path of a file</param>
/// <param name="rootPath"></param> /// <param name="rootPath"></param>
/// <param name="type">Library type to determine parsing to perform</param> /// <param name="type">Library type to determine parsing to perform</param>
public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type) /// <param name="enableMetadata">Enable Metadata parsing overriding filename parsing</param>
public ParserInfo? ParseFile(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata)
{ {
try try
{ {
var info = Parse(path, rootPath, libraryRoot, type); var info = Parse(path, rootPath, libraryRoot, type, enableMetadata);
if (info == null) if (info == null)
{ {
_logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path); _logger.LogError("Unable to parse any meaningful information out of file {FilePath}", path);
@ -174,28 +175,29 @@ public class ReadingItemService : IReadingItemService
/// <param name="path"></param> /// <param name="path"></param>
/// <param name="rootPath"></param> /// <param name="rootPath"></param>
/// <param name="type"></param> /// <param name="type"></param>
/// <param name="enableMetadata"></param>
/// <returns></returns> /// <returns></returns>
private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type) private ParserInfo? Parse(string path, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata)
{ {
if (_comicVineParser.IsApplicable(path, type)) if (_comicVineParser.IsApplicable(path, type))
{ {
return _comicVineParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _comicVineParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_imageParser.IsApplicable(path, type)) if (_imageParser.IsApplicable(path, type))
{ {
return _imageParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _imageParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_bookParser.IsApplicable(path, type)) if (_bookParser.IsApplicable(path, type))
{ {
return _bookParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _bookParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_pdfParser.IsApplicable(path, type)) if (_pdfParser.IsApplicable(path, type))
{ {
return _pdfParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _pdfParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
if (_basicParser.IsApplicable(path, type)) if (_basicParser.IsApplicable(path, type))
{ {
return _basicParser.Parse(path, rootPath, libraryRoot, type, GetComicInfo(path)); return _basicParser.Parse(path, rootPath, libraryRoot, type, enableMetadata, GetComicInfo(path));
} }
return null; return null;

View file

@ -804,7 +804,7 @@ public class ParseScannedFiles
{ {
// Process files sequentially // Process files sequentially
result.ParserInfos = files result.ParserInfos = files
.Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type)) .Select(file => _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata))
.Where(info => info != null) .Where(info => info != null)
.ToList()!; .ToList()!;
} }
@ -812,7 +812,7 @@ public class ParseScannedFiles
{ {
// Process files in parallel // Process files in parallel
var tasks = files.Select(file => Task.Run(() => var tasks = files.Select(file => Task.Run(() =>
_readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type))); _readingItemService.ParseFile(file, normalizedFolder, result.LibraryRoot, library.Type, library.EnableMetadata)));
var infos = await Task.WhenAll(tasks); var infos = await Task.WhenAll(tasks);
result.ParserInfos = infos.Where(info => info != null).ToList()!; result.ParserInfos = infos.Where(info => info != null).ToList()!;

View file

@ -12,7 +12,7 @@ namespace API.Services.Tasks.Scanner.Parser;
/// </summary> /// </summary>
public class BasicParser(IDirectoryService directoryService, IDefaultParser imageParser) : DefaultParser(directoryService) public class BasicParser(IDirectoryService directoryService, IDefaultParser imageParser) : DefaultParser(directoryService)
{ {
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null)
{ {
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); 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. // TODO: Potential Bug: This will return null, but on Image libraries, if all images, we would want to include this.
@ -20,7 +20,7 @@ public class BasicParser(IDirectoryService directoryService, IDefaultParser imag
if (Parser.IsImage(filePath)) if (Parser.IsImage(filePath))
{ {
return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, comicInfo); return imageParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Image, enableMetadata, comicInfo);
} }
var ret = new ParserInfo() var ret = new ParserInfo()

View file

@ -5,7 +5,7 @@ namespace API.Services.Tasks.Scanner.Parser;
public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService) public class BookParser(IDirectoryService directoryService, IBookService bookService, BasicParser basicParser) : DefaultParser(directoryService)
{ {
public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null) public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null)
{ {
var info = bookService.ParseInfo(filePath); var info = bookService.ParseInfo(filePath);
if (info == null) return null; if (info == null) return null;
@ -35,7 +35,7 @@ public class BookParser(IDirectoryService directoryService, IBookService bookSer
} }
else else
{ {
var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, comicInfo); var info2 = basicParser.Parse(filePath, rootPath, libraryRoot, LibraryType.Book, enableMetadata, comicInfo);
info.Merge(info2); info.Merge(info2);
if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series, type) if (hasVolumeInSeries && info2 != null && Parser.ParseVolume(info2.Series, type)
.Equals(Parser.LooseLeafVolume)) .Equals(Parser.LooseLeafVolume))

View file

@ -19,7 +19,7 @@ public class ComicVineParser(IDirectoryService directoryService) : DefaultParser
/// <param name="rootPath"></param> /// <param name="rootPath"></param>
/// <param name="type"></param> /// <param name="type"></param>
/// <returns></returns> /// <returns></returns>
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null)
{ {
if (type != LibraryType.ComicVine) return null; if (type != LibraryType.ComicVine) return null;

View file

@ -8,7 +8,7 @@ namespace API.Services.Tasks.Scanner.Parser;
public interface IDefaultParser public interface IDefaultParser
{ {
ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null); ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null);
void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret); void ParseFromFallbackFolders(string filePath, string rootPath, LibraryType type, ref ParserInfo ret);
bool IsApplicable(string filePath, LibraryType type); bool IsApplicable(string filePath, LibraryType type);
} }
@ -26,8 +26,9 @@ public abstract class DefaultParser(IDirectoryService directoryService) : IDefau
/// <param name="filePath"></param> /// <param name="filePath"></param>
/// <param name="rootPath">Root folder</param> /// <param name="rootPath">Root folder</param>
/// <param name="type">Allows different Regex to be used for parsing.</param> /// <param name="type">Allows different Regex to be used for parsing.</param>
/// <param name="enableMetadata">Allows overriding data from metadata (ComicInfo/pdf/epub)</param>
/// <returns><see cref="ParserInfo"/> or null if Series was empty</returns> /// <returns><see cref="ParserInfo"/> or null if Series was empty</returns>
public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null); public abstract ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null);
/// <summary> /// <summary>
/// Fills out <see cref="ParserInfo"/> by trying to parse volume, chapters, and series from folders /// Fills out <see cref="ParserInfo"/> by trying to parse volume, chapters, and series from folders

View file

@ -7,7 +7,7 @@ namespace API.Services.Tasks.Scanner.Parser;
public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService) public class ImageParser(IDirectoryService directoryService) : DefaultParser(directoryService)
{ {
public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo? comicInfo = null) public override ParserInfo? Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo? comicInfo = null)
{ {
if (!IsApplicable(filePath, type)) return null; if (!IsApplicable(filePath, type)) return null;

View file

@ -6,7 +6,7 @@ namespace API.Services.Tasks.Scanner.Parser;
public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService) public class PdfParser(IDirectoryService directoryService) : DefaultParser(directoryService)
{ {
public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, ComicInfo comicInfo = null) public override ParserInfo Parse(string filePath, string rootPath, string libraryRoot, LibraryType type, bool enableMetadata = true, ComicInfo comicInfo = null)
{ {
var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath); var fileName = directoryService.FileSystem.Path.GetFileNameWithoutExtension(filePath);
var ret = new ParserInfo var ret = new ParserInfo
@ -68,6 +68,8 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc
ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret); ParseFromFallbackFolders(filePath, tempRootPath, type, ref ret);
} }
if (enableMetadata)
{
// Patch in other information from ComicInfo // Patch in other information from ComicInfo
UpdateFromComicInfo(ret); UpdateFromComicInfo(ret);
@ -75,6 +77,8 @@ public class PdfParser(IDirectoryService directoryService) : DefaultParser(direc
{ {
ret.Title = comicInfo.Title.Trim(); ret.Title = comicInfo.Title.Trim();
} }
}
if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && type == LibraryType.Book) if (ret.Chapters == Parser.DefaultChapter && ret.Volumes == Parser.LooseLeafVolume && type == LibraryType.Book)
{ {

View file

@ -521,6 +521,11 @@ public class ScannerService : IScannerService
// Validations are done, now we can start actual scan // Validations are done, now we can start actual scan
_logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name);
if (!library.EnableMetadata)
{
_logger.LogInformation("[ScannerService] Warning! {LibraryName} has metadata turned off", library.Name);
}
// This doesn't work for something like M:/Manga/ and a series has library folder as root // This doesn't work for something like M:/Manga/ and a series has library folder as root
var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths)); var shouldUseLibraryScan = !(await _unitOfWork.LibraryRepository.DoAnySeriesFoldersMatch(libraryFolderPaths));
if (!shouldUseLibraryScan) if (!shouldUseLibraryScan)

View file

@ -0,0 +1,120 @@
import {AbstractControl, FormArray, FormControl, FormGroup} from '@angular/forms';
interface ValidationIssue {
path: string;
controlType: string;
value: any;
errors: { [key: string]: any } | null;
status: string;
disabled: boolean;
}
export function analyzeFormGroupValidation(formGroup: FormGroup, basePath: string = ''): ValidationIssue[] {
const issues: ValidationIssue[] = [];
function analyzeControl(control: AbstractControl, path: string): void {
// Determine control type for better debugging
let controlType = 'AbstractControl';
if (control instanceof FormGroup) {
controlType = 'FormGroup';
} else if (control instanceof FormArray) {
controlType = 'FormArray';
} else if (control instanceof FormControl) {
controlType = 'FormControl';
}
// Add issue if control has validation errors or is invalid
if (control.invalid || control.errors || control.disabled) {
issues.push({
path: path || 'root',
controlType,
value: control.value,
errors: control.errors,
status: control.status,
disabled: control.disabled
});
}
// Recursively check nested controls
if (control instanceof FormGroup) {
Object.keys(control.controls).forEach(key => {
const childPath = path ? `${path}.${key}` : key;
analyzeControl(control.controls[key], childPath);
});
} else if (control instanceof FormArray) {
control.controls.forEach((childControl, index) => {
const childPath = path ? `${path}[${index}]` : `[${index}]`;
analyzeControl(childControl, childPath);
});
}
}
analyzeControl(formGroup, basePath);
return issues;
}
export function printFormGroupValidation(formGroup: FormGroup, basePath: string = ''): void {
const issues = analyzeFormGroupValidation(formGroup, basePath);
console.group(`🔍 FormGroup Validation Analysis (${basePath || 'root'})`);
console.log(`Overall Status: ${formGroup.status}`);
console.log(`Overall Valid: ${formGroup.valid}`);
console.log(`Total Issues Found: ${issues.length}`);
if (issues.length === 0) {
console.log('✅ No validation issues found!');
} else {
console.log('\n📋 Detailed Issues:');
issues.forEach((issue, index) => {
console.group(`${index + 1}. ${issue.path} (${issue.controlType})`);
console.log(`Status: ${issue.status}`);
console.log(`Value:`, issue.value);
console.log(`Disabled: ${issue.disabled}`);
if (issue.errors) {
console.log('Validation Errors:');
Object.entries(issue.errors).forEach(([errorKey, errorValue]) => {
console.log(`${errorKey}:`, errorValue);
});
} else {
console.log('No specific validation errors (but control is invalid)');
}
console.groupEnd();
});
}
console.groupEnd();
}
// Alternative function that returns a formatted string instead of console logging
export function getFormGroupValidationReport(formGroup: FormGroup, basePath: string = ''): string {
const issues = analyzeFormGroupValidation(formGroup, basePath);
let report = `FormGroup Validation Report (${basePath || 'root'})\n`;
report += `Overall Status: ${formGroup.status}\n`;
report += `Overall Valid: ${formGroup.valid}\n`;
report += `Total Issues Found: ${issues.length}\n\n`;
if (issues.length === 0) {
report += '✅ No validation issues found!';
} else {
report += 'Detailed Issues:\n';
issues.forEach((issue, index) => {
report += `\n${index + 1}. ${issue.path} (${issue.controlType})\n`;
report += ` Status: ${issue.status}\n`;
report += ` Value: ${JSON.stringify(issue.value)}\n`;
report += ` Disabled: ${issue.disabled}\n`;
if (issue.errors) {
report += ' Validation Errors:\n';
Object.entries(issue.errors).forEach(([errorKey, errorValue]) => {
report += `${errorKey}: ${JSON.stringify(errorValue)}\n`;
});
} else {
report += ' No specific validation errors (but control is invalid)\n';
}
});
}
return report;
}

View file

@ -31,6 +31,7 @@ export interface Library {
manageReadingLists: boolean; manageReadingLists: boolean;
allowScrobbling: boolean; allowScrobbling: boolean;
allowMetadataMatching: boolean; allowMetadataMatching: boolean;
enableMetadata: boolean;
collapseSeriesRelationships: boolean; collapseSeriesRelationships: boolean;
libraryFileTypes: Array<FileTypeGroup>; libraryFileTypes: Array<FileTypeGroup>;
excludePatterns: Array<string>; excludePatterns: Array<string>;

View file

@ -127,6 +127,16 @@
</app-setting-item> </app-setting-item>
</div> </div>
<div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('enable-metadata-label')" [subtitle]="t('enable-metadata-tooltip')">
<ng-template #switch>
<div class="form-check form-switch float-end">
<input type="checkbox" id="enable-metadata" role="switch" formControlName="enableMetadata" class="form-check-input">
</div>
</ng-template>
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4"> <div class="row g-0 mt-4 mb-4">
<app-setting-switch [title]="t('manage-collection-label')" [subtitle]="t('manage-collection-tooltip')"> <app-setting-switch [title]="t('manage-collection-label')" [subtitle]="t('manage-collection-tooltip')">
<ng-template #switch> <ng-template #switch>

View file

@ -105,15 +105,16 @@ export class LibrarySettingsModalComponent implements OnInit {
libraryForm: FormGroup = new FormGroup({ libraryForm: FormGroup = new FormGroup({
name: new FormControl<string>('', { nonNullable: true, validators: [Validators.required] }), name: new FormControl<string>('', { nonNullable: true, validators: [Validators.required] }),
type: new FormControl<LibraryType>(LibraryType.Manga, { nonNullable: true, validators: [Validators.required] }), type: new FormControl<LibraryType>(LibraryType.Manga, { nonNullable: true, validators: [Validators.required] }),
folderWatching: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }), folderWatching: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
includeInDashboard: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }), includeInDashboard: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
includeInRecommended: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }), includeInRecommended: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
includeInSearch: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }), includeInSearch: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
manageCollections: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }), manageCollections: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
manageReadingLists: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }), manageReadingLists: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
allowScrobbling: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }), allowScrobbling: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
allowMetadataMatching: new FormControl<boolean>(true, { nonNullable: true, validators: [Validators.required] }), allowMetadataMatching: new FormControl<boolean>(true, { nonNullable: true, validators: [] }),
collapseSeriesRelationships: new FormControl<boolean>(false, { nonNullable: true, validators: [Validators.required] }), collapseSeriesRelationships: new FormControl<boolean>(false, { nonNullable: true, validators: [] }),
enableMetadata: new FormControl<boolean>(true, { nonNullable: true, validators: [] }), // required validator doesn't check value, just if true
}); });
selectedFolders: string[] = []; selectedFolders: string[] = [];
@ -155,7 +156,7 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get('allowScrobbling')?.disable(); this.libraryForm.get('allowScrobbling')?.disable();
if (this.IsMetadataDownloadEligible) { if (this.IsMetadataDownloadEligible) {
this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching); this.libraryForm.get('allowMetadataMatching')?.setValue(this.library.allowMetadataMatching ?? true);
this.libraryForm.get('allowMetadataMatching')?.enable(); this.libraryForm.get('allowMetadataMatching')?.enable();
} else { } else {
this.libraryForm.get('allowMetadataMatching')?.setValue(false); this.libraryForm.get('allowMetadataMatching')?.setValue(false);
@ -184,6 +185,20 @@ export class LibrarySettingsModalComponent implements OnInit {
this.setValues(); this.setValues();
// Turn on/off manage collections/rl
this.libraryForm.get('enableMetadata')?.valueChanges.pipe(
tap(enabled => {
const manageCollectionsFc = this.libraryForm.get('manageCollections');
const manageReadingListsFc = this.libraryForm.get('manageReadingLists');
manageCollectionsFc?.setValue(enabled);
manageReadingListsFc?.setValue(enabled);
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
// This needs to only apply after first render // This needs to only apply after first render
this.libraryForm.get('type')?.valueChanges.pipe( this.libraryForm.get('type')?.valueChanges.pipe(
tap((type: LibraryType) => { tap((type: LibraryType) => {
@ -257,6 +272,8 @@ export class LibrarySettingsModalComponent implements OnInit {
this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships); this.libraryForm.get('collapseSeriesRelationships')?.setValue(this.library.collapseSeriesRelationships);
this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false); this.libraryForm.get('allowScrobbling')?.setValue(this.IsKavitaPlusEligible ? this.library.allowScrobbling : false);
this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false); this.libraryForm.get('allowMetadataMatching')?.setValue(this.IsMetadataDownloadEligible ? this.library.allowMetadataMatching : false);
this.libraryForm.get('excludePatterns')?.setValue(this.excludePatterns ? this.library.excludePatterns : false);
this.libraryForm.get('enableMetadata')?.setValue(this.library.enableMetadata, true);
this.selectedFolders = this.library.folders; this.selectedFolders = this.library.folders;
this.madeChanges = false; this.madeChanges = false;

View file

@ -1129,6 +1129,8 @@
"include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.", "include-in-dashboard-tooltip": "Should series from the library be included on the Dashboard. This affects all streams, like On Deck, Recently Updated, Recently Added, or any custom additions.",
"include-in-search-label": "Include in Search", "include-in-search-label": "Include in Search",
"include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.", "include-in-search-tooltip": "Should series and any derived information (genres, people, files) from the library be included in search results.",
"enable-metadata-label": "Enable Metadata (ComicInfo/Epub/PDF)",
"enable-metadata-tooltip": "Allow Kavita to read metadata files which override filename parsing.",
"force-scan": "Force Scan", "force-scan": "Force Scan",
"force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan", "force-scan-tooltip": "This will force a scan on the library, treating like a fresh scan",
"reset": "{{common.reset}}", "reset": "{{common.reset}}",