diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index 9f45ca6195..e7f11e310a 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; @@ -20,7 +21,7 @@ namespace API.Tests; -public abstract class AbstractDbTest +public abstract class AbstractDbTest : IDisposable { protected readonly DbConnection _connection; protected readonly DataContext _context; @@ -28,6 +29,7 @@ public abstract class AbstractDbTest protected const string CacheDirectory = "C:/kavita/config/cache/"; + protected const string CacheLongDirectory = "C:/kavita/config/cache-long/"; protected const string CoverImageDirectory = "C:/kavita/config/covers/"; protected const string BackupDirectory = "C:/kavita/config/backups/"; protected const string LogDirectory = "C:/kavita/config/logs/"; @@ -38,21 +40,22 @@ public abstract class AbstractDbTest protected AbstractDbTest() { - var contextOptions = new DbContextOptionsBuilder() + var contextOptions = new DbContextOptionsBuilder() .UseSqlite(CreateInMemoryDatabase()) .Options; + _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; _context = new DataContext(contextOptions); + + _context.Database.EnsureCreated(); // Ensure DB schema is created + Task.Run(SeedDb).GetAwaiter().GetResult(); var config = new MapperConfiguration(cfg => cfg.AddProfile()); var mapper = config.CreateMapper(); - // Set up Hangfire to use in-memory storage for testing GlobalConfiguration.Configuration.UseInMemoryStorage(); - - _unitOfWork = new UnitOfWork(_context, mapper, null); } @@ -66,29 +69,40 @@ private static DbConnection CreateInMemoryDatabase() private async Task SeedDb() { - await _context.Database.MigrateAsync(); - var filesystem = CreateFileSystem(); + try + { + await _context.Database.MigrateAsync(); + var filesystem = CreateFileSystem(); - await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); + await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); - var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); - setting.Value = CacheDirectory; + var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); + setting.Value = CacheDirectory; - setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); - setting.Value = BackupDirectory; + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); + setting.Value = BackupDirectory; - setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); - setting.Value = BookmarkDirectory; + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); + setting.Value = BookmarkDirectory; - setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync(); - setting.Value = "10"; + setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync(); + setting.Value = "10"; - _context.ServerSetting.Update(setting); + _context.ServerSetting.Update(setting); - _context.Library.Add(new LibraryBuilder("Manga") - .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) - .Build()); - return await _context.SaveChangesAsync() > 0; + await Seed.SeedMetadataSettings(_context); + + _context.Library.Add(new LibraryBuilder("Manga") + .WithFolderPath(new FolderPathBuilder(DataDirectory).Build()) + .Build()); + + return await _context.SaveChangesAsync() > 0; + } + catch (Exception ex) + { + Console.WriteLine($"[SeedDb] Error: {ex.Message}"); + return false; + } } protected abstract Task ResetDb(); @@ -99,6 +113,7 @@ protected static MockFileSystem CreateFileSystem() fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); fileSystem.AddDirectory("C:/kavita/config/"); fileSystem.AddDirectory(CacheDirectory); + fileSystem.AddDirectory(CacheLongDirectory); fileSystem.AddDirectory(CoverImageDirectory); fileSystem.AddDirectory(BackupDirectory); fileSystem.AddDirectory(BookmarkDirectory); @@ -109,4 +124,10 @@ protected static MockFileSystem CreateFileSystem() return fileSystem; } + + public void Dispose() + { + _context.Dispose(); + _connection.Dispose(); + } } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 34b81c20bf..fbf0dea89c 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -81,7 +81,8 @@ public async Task AddLibrary(UpdateLibraryDto dto) .WithIncludeInDashboard(dto.IncludeInDashboard) .WithManageCollections(dto.ManageCollections) .WithManageReadingLists(dto.ManageReadingLists) - .WIthAllowScrobbling(dto.AllowScrobbling) + .WithAllowScrobbling(dto.AllowScrobbling) + .WithAllowMetadataMatching(dto.AllowMetadataMatching) .Build(); library.LibraryFileTypes = dto.FileGroupTypes diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index c8c85063e8..18dea94346 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -61,4 +61,10 @@ public class LibraryDto /// A set of globs that will exclude matching content from being scanned /// public ICollection ExcludePatterns { get; set; } + /// + /// Allow any series within this Library to download metadata. + /// + /// This does not exclude the library from being linked to wrt Series Relationships + /// Requires a valid LicenseKey + public bool AllowMetadataMatching { get; set; } = true; } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index 465782bd10..de02f304d2 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -26,6 +26,8 @@ public class UpdateLibraryDto public bool ManageReadingLists { get; init; } [Required] public bool AllowScrobbling { get; init; } + [Required] + public bool AllowMetadataMatching { get; init; } /// /// What types of files to allow the scanner to pickup /// diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 1e8cd37116..b6bd53a152 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -134,6 +134,9 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .Property(b => b.AllowScrobbling) .HasDefaultValue(true); + builder.Entity() + .Property(b => b.AllowMetadataMatching) + .HasDefaultValue(true); builder.Entity() .Property(b => b.WebLinks) diff --git a/API/Data/Migrations/20250201143355_KavitaPlusUserAndMetadataSettings.Designer.cs b/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs similarity index 99% rename from API/Data/Migrations/20250201143355_KavitaPlusUserAndMetadataSettings.Designer.cs rename to API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs index 1780dea176..835510a1e5 100644 --- a/API/Data/Migrations/20250201143355_KavitaPlusUserAndMetadataSettings.Designer.cs +++ b/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.Designer.cs @@ -11,7 +11,7 @@ namespace API.Data.Migrations { [DbContext(typeof(DataContext))] - [Migration("20250201143355_KavitaPlusUserAndMetadataSettings")] + [Migration("20250202163454_KavitaPlusUserAndMetadataSettings")] partial class KavitaPlusUserAndMetadataSettings { /// @@ -1132,6 +1132,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + b.Property("AllowScrobbling") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -1675,7 +1680,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("Enabled") - .HasColumnType("INTEGER"); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); b.Property("FirstLastPeopleNaming") .HasColumnType("INTEGER"); diff --git a/API/Data/Migrations/20250201143355_KavitaPlusUserAndMetadataSettings.cs b/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs similarity index 92% rename from API/Data/Migrations/20250201143355_KavitaPlusUserAndMetadataSettings.cs rename to API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs index 2f75ea2793..b23d7896bf 100644 --- a/API/Data/Migrations/20250201143355_KavitaPlusUserAndMetadataSettings.cs +++ b/API/Data/Migrations/20250202163454_KavitaPlusUserAndMetadataSettings.cs @@ -10,6 +10,13 @@ public partial class KavitaPlusUserAndMetadataSettings : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "AllowMetadataMatching", + table: "Library", + type: "INTEGER", + nullable: false, + defaultValue: true); + migrationBuilder.AddColumn( name: "AniListScrobblingEnabled", table: "AppUserPreferences", @@ -30,7 +37,7 @@ protected override void Up(MigrationBuilder migrationBuilder) { Id = table.Column(type: "INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - Enabled = table.Column(type: "INTEGER", nullable: false), + Enabled = table.Column(type: "INTEGER", nullable: false, defaultValue: true), EnableSummary = table.Column(type: "INTEGER", nullable: false), EnablePublicationStatus = table.Column(type: "INTEGER", nullable: false), EnableRelationships = table.Column(type: "INTEGER", nullable: false), @@ -89,6 +96,10 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "MetadataSettings"); + migrationBuilder.DropColumn( + name: "AllowMetadataMatching", + table: "Library"); + migrationBuilder.DropColumn( name: "AniListScrobblingEnabled", table: "AppUserPreferences"); diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index ea06e4aa4b..d969df2736 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1129,6 +1129,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + b.Property("AllowScrobbling") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") @@ -1672,7 +1677,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("Enabled") - .HasColumnType("INTEGER"); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); b.Property("FirstLastPeopleNaming") .HasColumnType("INTEGER"); diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 097c382d56..abab813781 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -40,8 +40,14 @@ public class Library : IEntityDate, IHasCoverImage /// /// Should this library allow Scrobble events to emit from it /// - /// Scrobbling requires a valid LicenseKey + /// Requires a valid LicenseKey public bool AllowScrobbling { get; set; } = true; + /// + /// Allow any series within this Library to download metadata. + /// + /// This does not exclude the library from being linked to wrt Series Relationships + /// Requires a valid LicenseKey + public bool AllowMetadataMatching { get; set; } = true; public DateTime Created { get; set; } diff --git a/API/Helpers/Builders/LibraryBuilder.cs b/API/Helpers/Builders/LibraryBuilder.cs index 5550cfd510..30e6136a53 100644 --- a/API/Helpers/Builders/LibraryBuilder.cs +++ b/API/Helpers/Builders/LibraryBuilder.cs @@ -104,7 +104,13 @@ public LibraryBuilder WithManageReadingLists(bool toInclude) return this; } - public LibraryBuilder WIthAllowScrobbling(bool allowScrobbling) + public LibraryBuilder WithAllowMetadataMatching(bool allow) + { + _library.AllowMetadataMatching = allow; + return this; + } + + public LibraryBuilder WithAllowScrobbling(bool allowScrobbling) { _library.AllowScrobbling = allowScrobbling; return this; diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index 48ca0bcbd6..80e7d8b6d5 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -385,7 +385,7 @@ public async Task UpdateSeriesDontMatch(int seriesId, bool dontMatch) private async Task FetchExternalMetadataForSeries(int seriesId, LibraryType libraryType, PlusSeriesRequestDto data) { - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Library); if (series == null) return _defaultReturn; try @@ -436,7 +436,7 @@ private async Task FetchExternalMetadataForSeries(int serie // If there is metadata and the user has metadata download turned on var madeMetadataModification = false; - if (result.Series != null) + if (result.Series != null && series.Library.AllowMetadataMatching) { madeMetadataModification = await WriteExternalMetadataToSeries(result.Series, seriesId); if (madeMetadataModification) diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index f834f23b14..87ffb56c40 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -24,6 +24,7 @@ export interface Library { manageCollections: boolean; manageReadingLists: boolean; allowScrobbling: boolean; + allowMetadataMatching: boolean; collapseSeriesRelationships: boolean; libraryFileTypes: Array; excludePatterns: Array; diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 700f439e51..69ba7c8271 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -157,6 +157,16 @@