diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index 3a4867ec4..9e7fc3a02 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -9,10 +9,10 @@ - - + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -28,6 +28,7 @@ + diff --git a/API.Tests/AbstractDbTest.cs b/API.Tests/AbstractDbTest.cs index 77f978e7f..9c5f3e726 100644 --- a/API.Tests/AbstractDbTest.cs +++ b/API.Tests/AbstractDbTest.cs @@ -20,10 +20,11 @@ namespace API.Tests; public abstract class AbstractDbTest : AbstractFsTest , IDisposable { - protected readonly DbConnection _connection; - protected readonly DataContext _context; - protected readonly IUnitOfWork _unitOfWork; - protected readonly IMapper _mapper; + protected readonly DataContext Context; + protected readonly IUnitOfWork UnitOfWork; + protected readonly IMapper Mapper; + private readonly DbConnection _connection; + private bool _disposed; protected AbstractDbTest() { @@ -34,17 +35,17 @@ public abstract class AbstractDbTest : AbstractFsTest , IDisposable _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; - _context = new DataContext(contextOptions); + Context = new DataContext(contextOptions); - _context.Database.EnsureCreated(); // Ensure DB schema is created + Context.Database.EnsureCreated(); // Ensure DB schema is created Task.Run(SeedDb).GetAwaiter().GetResult(); var config = new MapperConfiguration(cfg => cfg.AddProfile()); - _mapper = config.CreateMapper(); + Mapper = config.CreateMapper(); GlobalConfiguration.Configuration.UseInMemoryStorage(); - _unitOfWork = new UnitOfWork(_context, _mapper, null); + UnitOfWork = new UnitOfWork(Context, Mapper, null); } private static DbConnection CreateInMemoryDatabase() @@ -59,34 +60,34 @@ public abstract class AbstractDbTest : AbstractFsTest , IDisposable { try { - await _context.Database.EnsureCreatedAsync(); + await Context.Database.EnsureCreatedAsync(); 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(); + 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 = 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 = 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 = 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") + Context.Library.Add(new LibraryBuilder("Manga") .WithAllowMetadataMatching(true) .WithFolderPath(new FolderPathBuilder(DataDirectory).Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - await Seed.SeedMetadataSettings(_context); + await Seed.SeedMetadataSettings(Context); return true; } @@ -101,8 +102,21 @@ public abstract class AbstractDbTest : AbstractFsTest , IDisposable public void Dispose() { - _context.Dispose(); - _connection.Dispose(); + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + { + Context?.Dispose(); + _connection?.Dispose(); + } + + _disposed = true; } /// @@ -114,9 +128,9 @@ public abstract class AbstractDbTest : AbstractFsTest , IDisposable { var role = new AppRole { Id = userId, Name = roleName, NormalizedName = roleName.ToUpper() }; - await _context.Roles.AddAsync(role); - await _context.UserRoles.AddAsync(new AppUserRole { UserId = userId, RoleId = userId }); + await Context.Roles.AddAsync(role); + await Context.UserRoles.AddAsync(new AppUserRole { UserId = userId, RoleId = userId }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } } diff --git a/API.Tests/AbstractFsTest.cs b/API.Tests/AbstractFsTest.cs index 3341a3a7c..965a7ad78 100644 --- a/API.Tests/AbstractFsTest.cs +++ b/API.Tests/AbstractFsTest.cs @@ -1,6 +1,7 @@ using System.IO; +using System.IO.Abstractions; using System.IO.Abstractions.TestingHelpers; using API.Services.Tasks.Scanner.Parser; diff --git a/API.Tests/Extensions/ChapterListExtensionsTests.cs b/API.Tests/Extensions/ChapterListExtensionsTests.cs index d27903ca9..f19a0cede 100644 --- a/API.Tests/Extensions/ChapterListExtensionsTests.cs +++ b/API.Tests/Extensions/ChapterListExtensionsTests.cs @@ -142,7 +142,7 @@ public class ChapterListExtensionsTests CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false), }; - Assert.Equal(chapterList.First(), chapterList.GetFirstChapterWithFiles()); + Assert.Equal(chapterList[0], chapterList.GetFirstChapterWithFiles()); } [Fact] @@ -150,13 +150,13 @@ public class ChapterListExtensionsTests { var chapterList = new List() { - CreateChapter("darker than black", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), + CreateChapter("darker than black", Parser.DefaultChapter, CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), true), CreateChapter("darker than black", "1", CreateFile("/manga/darker than black.cbz", MangaFormat.Archive), false), }; - chapterList.First().Files = new List(); + chapterList[0].Files = new List(); - Assert.Equal(chapterList.Last(), chapterList.GetFirstChapterWithFiles()); + Assert.Equal(chapterList[^1], chapterList.GetFirstChapterWithFiles()); } @@ -181,7 +181,7 @@ public class ChapterListExtensionsTests CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; - chapterList[0].ReleaseDate = new DateTime(10, 1, 1); + chapterList[0].ReleaseDate = new DateTime(10, 1, 1, 0, 0, 0, DateTimeKind.Utc); chapterList[1].ReleaseDate = DateTime.MinValue; Assert.Equal(0, chapterList.MinimumReleaseYear()); @@ -196,8 +196,8 @@ public class ChapterListExtensionsTests CreateChapter("detective comics", API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, CreateFile("/manga/detective comics #001.cbz", MangaFormat.Archive), true) }; - chapterList[0].ReleaseDate = new DateTime(2002, 1, 1); - chapterList[1].ReleaseDate = new DateTime(2012, 2, 1); + chapterList[0].ReleaseDate = new DateTime(2002, 1, 1, 0, 0, 0, DateTimeKind.Utc); + chapterList[1].ReleaseDate = new DateTime(2012, 2, 1, 0, 0, 0, DateTimeKind.Utc); Assert.Equal(2002, chapterList.MinimumReleaseYear()); } diff --git a/API.Tests/Extensions/SeriesFilterTests.cs b/API.Tests/Extensions/SeriesFilterTests.cs index 8f64133bf..ba42be8a1 100644 --- a/API.Tests/Extensions/SeriesFilterTests.cs +++ b/API.Tests/Extensions/SeriesFilterTests.cs @@ -24,9 +24,9 @@ public class SeriesFilterTests : AbstractDbTest { protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series); - _context.AppUser.RemoveRange(_context.AppUser); - await _context.SaveChangesAsync(); + Context.Series.RemoveRange(Context.Series); + Context.AppUser.RemoveRange(Context.AppUser); + await Context.SaveChangesAsync(); } #region HasProgress @@ -54,18 +54,18 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); // Create read progress on Partial and Full - var readerService = new ReaderService(_unitOfWork, Substitute.For>(), + var readerService = new ReaderService(UnitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For()); // Select Partial and set pages read to 5 on first chapter - var partialSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2); + var partialSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2); var partialChapter = partialSeries.Volumes.First().Chapters.First(); Assert.True(await readerService.SaveReadingProgress(new ProgressDto() @@ -78,7 +78,7 @@ public class SeriesFilterTests : AbstractDbTest }, user.Id)); // Select Full and set pages read to 10 on first chapter - var fullSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(3); + var fullSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(3); var fullChapter = fullSeries.Volumes.First().Chapters.First(); Assert.True(await readerService.SaveReadingProgress(new ProgressDto() @@ -98,7 +98,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasProgress(); - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.LessThan, 50, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThan, 50, user.Id) .ToListAsync(); Assert.Single(queryResult); @@ -111,7 +111,7 @@ public class SeriesFilterTests : AbstractDbTest var user = await SetupHasProgress(); // Query series with progress <= 50% - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.LessThanEqual, 50, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThanEqual, 50, user.Id) .ToListAsync(); Assert.Equal(2, queryResult.Count); @@ -125,7 +125,7 @@ public class SeriesFilterTests : AbstractDbTest var user = await SetupHasProgress(); // Query series with progress > 50% - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.GreaterThan, 50, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.GreaterThan, 50, user.Id) .ToListAsync(); Assert.Single(queryResult); @@ -138,7 +138,7 @@ public class SeriesFilterTests : AbstractDbTest var user = await SetupHasProgress(); // Query series with progress == 100% - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.Equal, 100, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.Equal, 100, user.Id) .ToListAsync(); Assert.Single(queryResult); @@ -151,7 +151,7 @@ public class SeriesFilterTests : AbstractDbTest var user = await SetupHasProgress(); // Query series with progress < 100% - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.LessThan, 100, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThan, 100, user.Id) .ToListAsync(); Assert.Equal(2, queryResult.Count); @@ -165,7 +165,7 @@ public class SeriesFilterTests : AbstractDbTest var user = await SetupHasProgress(); // Query series with progress <= 100% - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.LessThanEqual, 100, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThanEqual, 100, user.Id) .ToListAsync(); Assert.Equal(3, queryResult.Count); @@ -188,16 +188,16 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); - var readerService = new ReaderService(_unitOfWork, Substitute.For>(), + var readerService = new ReaderService(UnitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For()); // Set progress to 99.99% (99/100 pages read) - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); var chapter = series.Volumes.First().Chapters.First(); Assert.True(await readerService.SaveReadingProgress(new ProgressDto() @@ -210,7 +210,7 @@ public class SeriesFilterTests : AbstractDbTest }, user.Id)); // Query series with progress < 100% - var queryResult = await _context.Series.HasReadingProgress(true, FilterComparison.LessThan, 100, user.Id) + var queryResult = await Context.Series.HasReadingProgress(true, FilterComparison.LessThan, 100, user.Id) .ToListAsync(); Assert.Single(queryResult); @@ -246,9 +246,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -258,7 +258,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.Equal, ["en"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Equal, ["en"]).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("en", foundSeries[0].Metadata.Language); } @@ -268,7 +268,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.NotEqual, ["en"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.NotEqual, ["en"]).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.DoesNotContain(foundSeries, s => s.Metadata.Language == "en"); } @@ -278,7 +278,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.Contains, ["en", "fr"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Contains, ["en", "fr"]).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Metadata.Language == "en"); Assert.Contains(foundSeries, s => s.Metadata.Language == "fr"); @@ -289,7 +289,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.NotContains, ["en", "fr"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.NotContains, ["en", "fr"]).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("es", foundSeries[0].Metadata.Language); } @@ -300,11 +300,11 @@ public class SeriesFilterTests : AbstractDbTest await SetupHasLanguage(); // Since "MustContains" matches all the provided languages, no series should match in this case. - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.MustContains, ["en", "fr"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.MustContains, ["en", "fr"]).ToListAsync(); Assert.Empty(foundSeries); // Single language should work. - foundSeries = await _context.Series.HasLanguage(true, FilterComparison.MustContains, ["en"]).ToListAsync(); + foundSeries = await Context.Series.HasLanguage(true, FilterComparison.MustContains, ["en"]).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("en", foundSeries[0].Metadata.Language); } @@ -314,7 +314,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.Matches, ["e"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Matches, ["e"]).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains("en", foundSeries.Select(s => s.Metadata.Language)); Assert.Contains("es", foundSeries.Select(s => s.Metadata.Language)); @@ -325,7 +325,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(false, FilterComparison.Equal, ["en"]).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(false, FilterComparison.Equal, ["en"]).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -334,7 +334,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasLanguage(); - var foundSeries = await _context.Series.HasLanguage(true, FilterComparison.Equal, new List()).ToListAsync(); + var foundSeries = await Context.Series.HasLanguage(true, FilterComparison.Equal, new List()).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -345,7 +345,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasLanguage(true, FilterComparison.GreaterThan, ["en"]).ToListAsync(); + await Context.Series.HasLanguage(true, FilterComparison.GreaterThan, ["en"]).ToListAsync(); }); } @@ -379,9 +379,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -391,7 +391,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.Equal, 100).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.Equal, 100).ToListAsync(); Assert.Single(series); Assert.Equal("Full", series[0].Name); } @@ -401,7 +401,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.GreaterThan, 50).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.GreaterThan, 50).ToListAsync(); Assert.Single(series); Assert.Equal("Full", series[0].Name); } @@ -411,7 +411,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.GreaterThanEqual, 50).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.GreaterThanEqual, 50).ToListAsync(); Assert.Equal(2, series.Count); Assert.Contains(series, s => s.Name == "Partial"); Assert.Contains(series, s => s.Name == "Full"); @@ -422,7 +422,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.LessThan, 50).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.LessThan, 50).ToListAsync(); Assert.Single(series); Assert.Equal("None", series[0].Name); } @@ -432,7 +432,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.LessThanEqual, 50).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.LessThanEqual, 50).ToListAsync(); Assert.Equal(2, series.Count); Assert.Contains(series, s => s.Name == "None"); Assert.Contains(series, s => s.Name == "Partial"); @@ -443,7 +443,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.NotEqual, 100).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.NotEqual, 100).ToListAsync(); Assert.Equal(2, series.Count); Assert.DoesNotContain(series, s => s.Name == "Full"); } @@ -453,7 +453,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(false, FilterComparison.Equal, 100).ToListAsync(); + var series = await Context.Series.HasAverageRating(false, FilterComparison.Equal, 100).ToListAsync(); Assert.Equal(3, series.Count); } @@ -462,7 +462,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAverageRating(); - var series = await _context.Series.HasAverageRating(true, FilterComparison.Equal, -1).ToListAsync(); + var series = await Context.Series.HasAverageRating(true, FilterComparison.Equal, -1).ToListAsync(); Assert.Single(series); Assert.Equal("None", series[0].Name); } @@ -474,7 +474,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasAverageRating(true, FilterComparison.Contains, 50).ToListAsync(); + await Context.Series.HasAverageRating(true, FilterComparison.Contains, 50).ToListAsync(); }); } @@ -485,7 +485,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasAverageRating(true, (FilterComparison)999, 50).ToListAsync(); + await Context.Series.HasAverageRating(true, (FilterComparison)999, 50).ToListAsync(); }); } @@ -519,9 +519,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -531,7 +531,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasPublicationStatus(); - var foundSeries = await _context.Series.HasPublicationStatus(true, FilterComparison.Equal, new List { PublicationStatus.Cancelled }).ToListAsync(); + var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.Equal, new List { PublicationStatus.Cancelled }).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("Cancelled", foundSeries[0].Name); } @@ -541,7 +541,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasPublicationStatus(); - var foundSeries = await _context.Series.HasPublicationStatus(true, FilterComparison.Contains, new List { PublicationStatus.Cancelled, PublicationStatus.Completed }).ToListAsync(); + var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.Contains, new List { PublicationStatus.Cancelled, PublicationStatus.Completed }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "Cancelled"); Assert.Contains(foundSeries, s => s.Name == "Completed"); @@ -552,7 +552,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasPublicationStatus(); - var foundSeries = await _context.Series.HasPublicationStatus(true, FilterComparison.NotContains, new List { PublicationStatus.Cancelled }).ToListAsync(); + var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.NotContains, new List { PublicationStatus.Cancelled }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "OnGoing"); Assert.Contains(foundSeries, s => s.Name == "Completed"); @@ -563,7 +563,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasPublicationStatus(); - var foundSeries = await _context.Series.HasPublicationStatus(true, FilterComparison.NotEqual, new List { PublicationStatus.OnGoing }).ToListAsync(); + var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.NotEqual, new List { PublicationStatus.OnGoing }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "Cancelled"); Assert.Contains(foundSeries, s => s.Name == "Completed"); @@ -574,7 +574,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasPublicationStatus(); - var foundSeries = await _context.Series.HasPublicationStatus(false, FilterComparison.Equal, new List { PublicationStatus.Cancelled }).ToListAsync(); + var foundSeries = await Context.Series.HasPublicationStatus(false, FilterComparison.Equal, new List { PublicationStatus.Cancelled }).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -583,7 +583,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasPublicationStatus(); - var foundSeries = await _context.Series.HasPublicationStatus(true, FilterComparison.Equal, new List()).ToListAsync(); + var foundSeries = await Context.Series.HasPublicationStatus(true, FilterComparison.Equal, new List()).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -594,7 +594,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasPublicationStatus(true, FilterComparison.BeginsWith, new List { PublicationStatus.Cancelled }).ToListAsync(); + await Context.Series.HasPublicationStatus(true, FilterComparison.BeginsWith, new List { PublicationStatus.Cancelled }).ToListAsync(); }); } @@ -605,7 +605,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasPublicationStatus(true, (FilterComparison)999, new List { PublicationStatus.Cancelled }).ToListAsync(); + await Context.Series.HasPublicationStatus(true, (FilterComparison)999, new List { PublicationStatus.Cancelled }).ToListAsync(); }); } #endregion @@ -637,9 +637,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -649,7 +649,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.Equal, [AgeRating.G]).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.Equal, [AgeRating.G]).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("G", foundSeries[0].Name); } @@ -659,7 +659,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.Contains, new List { AgeRating.G, AgeRating.Mature }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.Contains, new List { AgeRating.G, AgeRating.Mature }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "G"); Assert.Contains(foundSeries, s => s.Name == "Mature"); @@ -670,7 +670,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.NotContains, new List { AgeRating.Unknown }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.NotContains, new List { AgeRating.Unknown }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "G"); Assert.Contains(foundSeries, s => s.Name == "Mature"); @@ -681,7 +681,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.NotEqual, new List { AgeRating.G }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.NotEqual, new List { AgeRating.G }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "Unknown"); Assert.Contains(foundSeries, s => s.Name == "Mature"); @@ -692,7 +692,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.GreaterThan, new List { AgeRating.Unknown }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.GreaterThan, new List { AgeRating.Unknown }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "G"); Assert.Contains(foundSeries, s => s.Name == "Mature"); @@ -703,7 +703,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.GreaterThanEqual, new List { AgeRating.G }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.GreaterThanEqual, new List { AgeRating.G }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "G"); Assert.Contains(foundSeries, s => s.Name == "Mature"); @@ -714,7 +714,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.LessThan, new List { AgeRating.Mature }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.LessThan, new List { AgeRating.Mature }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "Unknown"); Assert.Contains(foundSeries, s => s.Name == "G"); @@ -725,7 +725,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.LessThanEqual, new List { AgeRating.G }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.LessThanEqual, new List { AgeRating.G }).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "Unknown"); Assert.Contains(foundSeries, s => s.Name == "G"); @@ -736,7 +736,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(false, FilterComparison.Equal, new List { AgeRating.G }).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(false, FilterComparison.Equal, new List { AgeRating.G }).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -745,7 +745,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasAgeRating(); - var foundSeries = await _context.Series.HasAgeRating(true, FilterComparison.Equal, new List()).ToListAsync(); + var foundSeries = await Context.Series.HasAgeRating(true, FilterComparison.Equal, new List()).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -756,7 +756,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasAgeRating(true, FilterComparison.BeginsWith, new List { AgeRating.G }).ToListAsync(); + await Context.Series.HasAgeRating(true, FilterComparison.BeginsWith, new List { AgeRating.G }).ToListAsync(); }); } @@ -767,7 +767,7 @@ public class SeriesFilterTests : AbstractDbTest await Assert.ThrowsAsync(async () => { - await _context.Series.HasAgeRating(true, (FilterComparison)999, new List { AgeRating.G }).ToListAsync(); + await Context.Series.HasAgeRating(true, (FilterComparison)999, new List { AgeRating.G }).ToListAsync(); }); } @@ -801,9 +801,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -813,7 +813,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.Equal, 2020).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.Equal, 2020).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("2020", foundSeries[0].Name); } @@ -823,7 +823,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.GreaterThan, 2000).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.GreaterThan, 2000).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "2020"); Assert.Contains(foundSeries, s => s.Name == "2025"); @@ -834,7 +834,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.LessThan, 2025).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.LessThan, 2025).ToListAsync(); Assert.Equal(2, foundSeries.Count); Assert.Contains(foundSeries, s => s.Name == "2000"); Assert.Contains(foundSeries, s => s.Name == "2020"); @@ -845,7 +845,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.IsInLast, 5).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.IsInLast, 5).ToListAsync(); Assert.Equal(2, foundSeries.Count); } @@ -854,7 +854,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.IsNotInLast, 5).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.IsNotInLast, 5).ToListAsync(); Assert.Single(foundSeries); Assert.Contains(foundSeries, s => s.Name == "2000"); } @@ -864,7 +864,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(false, FilterComparison.Equal, 2020).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(false, FilterComparison.Equal, 2020).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -873,7 +873,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasReleaseYear(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.Equal, null).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.Equal, null).ToListAsync(); Assert.Equal(3, foundSeries.Count); } @@ -889,10 +889,10 @@ public class SeriesFilterTests : AbstractDbTest .Build()) .Build(); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Library.Add(library); + await Context.SaveChangesAsync(); - var foundSeries = await _context.Series.HasReleaseYear(true, FilterComparison.IsEmpty, 0).ToListAsync(); + var foundSeries = await Context.Series.HasReleaseYear(true, FilterComparison.IsEmpty, 0).ToListAsync(); Assert.Single(foundSeries); Assert.Equal("EmptyYear", foundSeries[0].Name); } @@ -925,14 +925,14 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); - var ratingService = new RatingService(_unitOfWork, Substitute.For(), Substitute.For>()); + var ratingService = new RatingService(UnitOfWork, Substitute.For(), Substitute.For>()); // Select 0 Rating - var zeroRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2); + var zeroRating = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2); Assert.NotNull(zeroRating); Assert.True(await ratingService.UpdateSeriesRating(user, new UpdateRatingDto() @@ -942,7 +942,7 @@ public class SeriesFilterTests : AbstractDbTest })); // Select 4.5 Rating - var partialRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(3); + var partialRating = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(3); Assert.True(await ratingService.UpdateSeriesRating(user, new UpdateRatingDto() { @@ -958,7 +958,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasRating(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasRating(true, FilterComparison.Equal, 4.5f, user.Id) .ToListAsync(); @@ -971,7 +971,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasRating(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasRating(true, FilterComparison.GreaterThan, 0, user.Id) .ToListAsync(); @@ -984,7 +984,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasRating(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasRating(true, FilterComparison.LessThan, 4.5f, user.Id) .ToListAsync(); @@ -997,7 +997,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasRating(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasRating(true, FilterComparison.IsEmpty, 0, user.Id) .ToListAsync(); @@ -1010,7 +1010,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasRating(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasRating(true, FilterComparison.GreaterThanEqual, 4.5f, user.Id) .ToListAsync(); @@ -1023,7 +1023,7 @@ public class SeriesFilterTests : AbstractDbTest { var user = await SetupHasRating(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasRating(true, FilterComparison.LessThanEqual, 0, user.Id) .ToListAsync(); @@ -1101,9 +1101,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -1113,7 +1113,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.Equal, "My Dress-Up Darling") .ToListAsync(); @@ -1126,7 +1126,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.Equal, "Ijiranaide, Nagatoro-san") .ToListAsync(); @@ -1139,7 +1139,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.BeginsWith, "My Dress") .ToListAsync(); @@ -1152,7 +1152,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.BeginsWith, "Sono Bisque") .ToListAsync(); @@ -1165,7 +1165,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.EndsWith, "Nagatoro") .ToListAsync(); @@ -1178,7 +1178,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.Matches, "Toy With Me") .ToListAsync(); @@ -1191,7 +1191,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasName(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasName(true, FilterComparison.NotEqual, "My Dress-Up Darling") .ToListAsync(); @@ -1235,9 +1235,9 @@ public class SeriesFilterTests : AbstractDbTest .WithLibrary(library) .Build(); - _context.Users.Add(user); - _context.Library.Add(library); - await _context.SaveChangesAsync(); + Context.Users.Add(user); + Context.Library.Add(library); + await Context.SaveChangesAsync(); return user; } @@ -1247,7 +1247,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasSummary(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasSummary(true, FilterComparison.Equal, "I like hippos") .ToListAsync(); @@ -1260,7 +1260,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasSummary(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasSummary(true, FilterComparison.BeginsWith, "I like h") .ToListAsync(); @@ -1273,7 +1273,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasSummary(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasSummary(true, FilterComparison.EndsWith, "apples") .ToListAsync(); @@ -1286,7 +1286,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasSummary(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasSummary(true, FilterComparison.Matches, "like ducks") .ToListAsync(); @@ -1299,7 +1299,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasSummary(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasSummary(true, FilterComparison.NotEqual, "I like ducks") .ToListAsync(); @@ -1312,7 +1312,7 @@ public class SeriesFilterTests : AbstractDbTest { await SetupHasSummary(); - var foundSeries = await _context.Series + var foundSeries = await Context.Series .HasSummary(true, FilterComparison.IsEmpty, string.Empty) .ToListAsync(); diff --git a/API.Tests/Helpers/PersonHelperTests.cs b/API.Tests/Helpers/PersonHelperTests.cs index 1a38ccdac..47dab48da 100644 --- a/API.Tests/Helpers/PersonHelperTests.cs +++ b/API.Tests/Helpers/PersonHelperTests.cs @@ -1,5 +1,10 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using API.Entities.Enums; +using API.Helpers; +using API.Helpers.Builders; +using Xunit; namespace API.Tests.Helpers; @@ -7,127 +12,215 @@ public class PersonHelperTests : AbstractDbTest { protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series.ToList()); - await _context.SaveChangesAsync(); + Context.Series.RemoveRange(Context.Series.ToList()); + Context.Person.RemoveRange(Context.Person.ToList()); + Context.Library.RemoveRange(Context.Library.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); + await Context.SaveChangesAsync(); } - // - // // 1. Test adding new people and keeping existing ones - // [Fact] - // public async Task UpdateChapterPeopleAsync_AddNewPeople_ExistingPersonRetained() - // { - // var existingPerson = new PersonBuilder("Joe Shmo").Build(); - // var chapter = new ChapterBuilder("1").Build(); - // - // // Create an existing person and assign them to the series with a role - // var series = new SeriesBuilder("Test 1") - // .WithFormat(MangaFormat.Archive) - // .WithMetadata(new SeriesMetadataBuilder() - // .WithPerson(existingPerson, PersonRole.Editor) - // .Build()) - // .WithVolume(new VolumeBuilder("1").WithChapter(chapter).Build()) - // .Build(); - // - // _unitOfWork.SeriesRepository.Add(series); - // await _unitOfWork.CommitAsync(); - // - // // Call UpdateChapterPeopleAsync with one existing and one new person - // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo", "New Person" }, PersonRole.Editor, _unitOfWork); - // - // // Assert existing person retained and new person added - // var people = await _unitOfWork.PersonRepository.GetAllPeople(); - // Assert.Contains(people, p => p.Name == "Joe Shmo"); - // Assert.Contains(people, p => p.Name == "New Person"); - // - // var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - // Assert.Contains("Joe Shmo", chapterPeople); - // Assert.Contains("New Person", chapterPeople); - // } - // - // // 2. Test removing a person no longer in the list - // [Fact] - // public async Task UpdateChapterPeopleAsync_RemovePeople() - // { - // var existingPerson1 = new PersonBuilder("Joe Shmo").Build(); - // var existingPerson2 = new PersonBuilder("Jane Doe").Build(); - // var chapter = new ChapterBuilder("1").Build(); - // - // var series = new SeriesBuilder("Test 1") - // .WithVolume(new VolumeBuilder("1") - // .WithChapter(new ChapterBuilder("1") - // .WithPerson(existingPerson1, PersonRole.Editor) - // .WithPerson(existingPerson2, PersonRole.Editor) - // .Build()) - // .Build()) - // .Build(); - // - // _unitOfWork.SeriesRepository.Add(series); - // await _unitOfWork.CommitAsync(); - // - // // Call UpdateChapterPeopleAsync with only one person - // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, _unitOfWork); - // - // var people = await _unitOfWork.PersonRepository.GetAllPeople(); - // Assert.DoesNotContain(people, p => p.Name == "Jane Doe"); - // - // var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - // Assert.Contains("Joe Shmo", chapterPeople); - // Assert.DoesNotContain("Jane Doe", chapterPeople); - // } - // - // // 3. Test no changes when the list of people is the same - // [Fact] - // public async Task UpdateChapterPeopleAsync_NoChanges() - // { - // var existingPerson = new PersonBuilder("Joe Shmo").Build(); - // var chapter = new ChapterBuilder("1").Build(); - // - // var series = new SeriesBuilder("Test 1") - // .WithVolume(new VolumeBuilder("1") - // .WithChapter(new ChapterBuilder("1") - // .WithPerson(existingPerson, PersonRole.Editor) - // .Build()) - // .Build()) - // .Build(); - // - // _unitOfWork.SeriesRepository.Add(series); - // await _unitOfWork.CommitAsync(); - // - // // Call UpdateChapterPeopleAsync with the same list - // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, _unitOfWork); - // - // var people = await _unitOfWork.PersonRepository.GetAllPeople(); - // Assert.Contains(people, p => p.Name == "Joe Shmo"); - // - // var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); - // Assert.Contains("Joe Shmo", chapterPeople); - // Assert.Single(chapter.People); // No duplicate entries - // } - // - // // 4. Test multiple roles for a person - // [Fact] - // public async Task UpdateChapterPeopleAsync_MultipleRoles() - // { - // var person = new PersonBuilder("Joe Shmo").Build(); - // var chapter = new ChapterBuilder("1").Build(); - // - // var series = new SeriesBuilder("Test 1") - // .WithVolume(new VolumeBuilder("1") - // .WithChapter(new ChapterBuilder("1") - // .WithPerson(person, PersonRole.Writer) // Assign person as Writer - // .Build()) - // .Build()) - // .Build(); - // - // _unitOfWork.SeriesRepository.Add(series); - // await _unitOfWork.CommitAsync(); - // - // // Add same person as Editor - // await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, _unitOfWork); - // - // // Ensure that the same person is assigned with two roles - // var chapterPeople = chapter.People.Where(cp => cp.Person.Name == "Joe Shmo").ToList(); - // Assert.Equal(2, chapterPeople.Count); // One for each role - // Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Writer); - // Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Editor); - // } + + // 1. Test adding new people and keeping existing ones + [Fact] + public async Task UpdateChapterPeopleAsync_AddNewPeople_ExistingPersonRetained() + { + await ResetDb(); + + var library = new LibraryBuilder("My Library") + .Build(); + + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var existingPerson = new PersonBuilder("Joe Shmo").Build(); + var chapter = new ChapterBuilder("1").Build(); + + // Create an existing person and assign them to the series with a role + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithFormat(MangaFormat.Archive) + .WithMetadata(new SeriesMetadataBuilder() + .WithPerson(existingPerson, PersonRole.Editor) + .Build()) + .WithVolume(new VolumeBuilder("1").WithChapter(chapter).Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + await UnitOfWork.CommitAsync(); + + // Call UpdateChapterPeopleAsync with one existing and one new person + await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo", "New Person" }, PersonRole.Editor, UnitOfWork); + + // Assert existing person retained and new person added + var people = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Contains(people, p => p.Name == "Joe Shmo"); + Assert.Contains(people, p => p.Name == "New Person"); + + var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); + Assert.Contains("Joe Shmo", chapterPeople); + Assert.Contains("New Person", chapterPeople); + } + + // 2. Test removing a person no longer in the list + [Fact] + public async Task UpdateChapterPeopleAsync_RemovePeople() + { + await ResetDb(); + + var library = new LibraryBuilder("My Library") + .Build(); + + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var existingPerson1 = new PersonBuilder("Joe Shmo").Build(); + var existingPerson2 = new PersonBuilder("Jane Doe").Build(); + var chapter = new ChapterBuilder("1") + .WithPerson(existingPerson1, PersonRole.Editor) + .WithPerson(existingPerson2, PersonRole.Editor) + .Build(); + + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + await UnitOfWork.CommitAsync(); + + // Call UpdateChapterPeopleAsync with only one person + await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); + + // PersonHelper does not remove the Person from the global DbSet itself + await UnitOfWork.PersonRepository.RemoveAllPeopleNoLongerAssociated(); + + var people = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.DoesNotContain(people, p => p.Name == "Jane Doe"); + + var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); + Assert.Contains("Joe Shmo", chapterPeople); + Assert.DoesNotContain("Jane Doe", chapterPeople); + } + + // 3. Test no changes when the list of people is the same + [Fact] + public async Task UpdateChapterPeopleAsync_NoChanges() + { + await ResetDb(); + + var library = new LibraryBuilder("My Library") + .Build(); + + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var existingPerson = new PersonBuilder("Joe Shmo").Build(); + var chapter = new ChapterBuilder("1").WithPerson(existingPerson, PersonRole.Editor).Build(); + + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + await UnitOfWork.CommitAsync(); + + // Call UpdateChapterPeopleAsync with the same list + await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); + + var people = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Contains(people, p => p.Name == "Joe Shmo"); + + var chapterPeople = chapter.People.Select(cp => cp.Person.Name).ToList(); + Assert.Contains("Joe Shmo", chapterPeople); + Assert.Single(chapter.People); // No duplicate entries + } + + // 4. Test multiple roles for a person + [Fact] + public async Task UpdateChapterPeopleAsync_MultipleRoles() + { + await ResetDb(); + + var library = new LibraryBuilder("My Library") + .Build(); + + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var person = new PersonBuilder("Joe Shmo").Build(); + var chapter = new ChapterBuilder("1").WithPerson(person, PersonRole.Writer).Build(); + + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + await UnitOfWork.CommitAsync(); + + // Add same person as Editor + await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Shmo" }, PersonRole.Editor, UnitOfWork); + + // Ensure that the same person is assigned with two roles + var chapterPeople = chapter + .People + .Where(cp => + cp.Person.Name == "Joe Shmo") + .ToList(); + Assert.Equal(2, chapterPeople.Count); // One for each role + Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Writer); + Assert.Contains(chapterPeople, cp => cp.Role == PersonRole.Editor); + } + + [Fact] + public async Task UpdateChapterPeopleAsync_MatchOnAlias_NoChanges() + { + await ResetDb(); + + var library = new LibraryBuilder("My Library") + .Build(); + + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var person = new PersonBuilder("Joe Doe") + .WithAlias("Jonny Doe") + .Build(); + + var chapter = new ChapterBuilder("1") + .WithPerson(person, PersonRole.Editor) + .Build(); + + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + await UnitOfWork.CommitAsync(); + + // Add on Name + await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Joe Doe" }, PersonRole.Editor, UnitOfWork); + await UnitOfWork.CommitAsync(); + + var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Single(allPeople); + + // Add on alias + await PersonHelper.UpdateChapterPeopleAsync(chapter, new List { "Jonny Doe" }, PersonRole.Editor, UnitOfWork); + await UnitOfWork.CommitAsync(); + + allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Single(allPeople); + } + + // TODO: Unit tests for series } diff --git a/API.Tests/Services/CleanupServiceTests.cs b/API.Tests/Services/CleanupServiceTests.cs index 0f1e9e9da..b0610aed5 100644 --- a/API.Tests/Services/CleanupServiceTests.cs +++ b/API.Tests/Services/CleanupServiceTests.cs @@ -29,11 +29,11 @@ public class CleanupServiceTests : AbstractDbTest public CleanupServiceTests() : base() { - _context.Library.Add(new LibraryBuilder("Manga") + Context.Library.Add(new LibraryBuilder("Manga") .WithFolderPath(new FolderPathBuilder(Root + "data/").Build()) .Build()); - _readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For(), + _readerService = new ReaderService(UnitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); } @@ -43,11 +43,11 @@ public class CleanupServiceTests : AbstractDbTest protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series.ToList()); - _context.Users.RemoveRange(_context.Users.ToList()); - _context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); + Context.Users.RemoveRange(Context.Users.ToList()); + Context.AppUserBookmark.RemoveRange(Context.AppUserBookmark.ToList()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } #endregion @@ -68,18 +68,18 @@ public class CleanupServiceTests : AbstractDbTest var s = new SeriesBuilder("Test 1").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; - _context.Series.Add(s); + Context.Series.Add(s); s = new SeriesBuilder("Test 2").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; - _context.Series.Add(s); + Context.Series.Add(s); s = new SeriesBuilder("Test 3").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg"; s.LibraryId = 1; - _context.Series.Add(s); + Context.Series.Add(s); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.DeleteSeriesCoverImages(); @@ -102,16 +102,16 @@ public class CleanupServiceTests : AbstractDbTest var s = new SeriesBuilder("Test 1").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; - _context.Series.Add(s); + Context.Series.Add(s); s = new SeriesBuilder("Test 2").Build(); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; - _context.Series.Add(s); + Context.Series.Add(s); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.DeleteSeriesCoverImages(); @@ -133,7 +133,7 @@ public class CleanupServiceTests : AbstractDbTest await ResetDb(); // Add 2 series with cover images - _context.Series.Add(new SeriesBuilder("Test 1") + Context.Series.Add(new SeriesBuilder("Test 1") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c01.jpg").Build()) .WithCoverImage("v01_c01.jpg") @@ -142,7 +142,7 @@ public class CleanupServiceTests : AbstractDbTest .WithLibraryId(1) .Build()); - _context.Series.Add(new SeriesBuilder("Test 2") + Context.Series.Add(new SeriesBuilder("Test 2") .WithVolume(new VolumeBuilder("1") .WithChapter(new ChapterBuilder(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter).WithCoverImage("v01_c03.jpg").Build()) .WithCoverImage("v01_c03.jpg") @@ -152,9 +152,9 @@ public class CleanupServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.DeleteChapterCoverImages(); @@ -223,7 +223,7 @@ public class CleanupServiceTests : AbstractDbTest // Delete all Series to reset state await ResetDb(); - _context.Users.Add(new AppUser() + Context.Users.Add(new AppUser() { UserName = "Joe", ReadingLists = new List() @@ -239,9 +239,9 @@ public class CleanupServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.DeleteReadingListCoverImages(); @@ -260,7 +260,7 @@ public class CleanupServiceTests : AbstractDbTest filesystem.AddFile($"{CacheDirectory}02.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); cleanupService.CleanupCacheAndTempDirectories(); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories)); @@ -274,7 +274,7 @@ public class CleanupServiceTests : AbstractDbTest filesystem.AddFile($"{CacheDirectory}subdir/02.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); cleanupService.CleanupCacheAndTempDirectories(); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories)); @@ -297,7 +297,7 @@ public class CleanupServiceTests : AbstractDbTest filesystem.AddFile($"{BackupDirectory}randomfile.zip", filesystemFile); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.CleanupBackups(); Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); @@ -319,7 +319,7 @@ public class CleanupServiceTests : AbstractDbTest }); var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.CleanupBackups(); Assert.True(filesystem.File.Exists($"{BackupDirectory}randomfile.zip")); @@ -343,7 +343,7 @@ public class CleanupServiceTests : AbstractDbTest } var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.CleanupLogs(); Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); @@ -372,7 +372,7 @@ public class CleanupServiceTests : AbstractDbTest var ds = new DirectoryService(Substitute.For>(), filesystem); - var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, + var cleanupService = new CleanupService(_logger, UnitOfWork, _messageHub, ds); await cleanupService.CleanupLogs(); Assert.True(filesystem.File.Exists($"{LogDirectory}kavita20200911.log")); @@ -396,36 +396,36 @@ public class CleanupServiceTests : AbstractDbTest .Build(); series.Library = new LibraryBuilder("Test LIb").Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 5); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); - var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); // Delete the Chapter - _context.Chapter.Remove(c); - await _unitOfWork.CommitAsync(); - Assert.Empty(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); + Context.Chapter.Remove(c); + await UnitOfWork.CommitAsync(); + Assert.Empty(await UnitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); // NOTE: This may not be needed, the underlying DB structure seems fixed as of v0.7 await cleanupService.CleanupDbEntries(); - Assert.Empty(await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); + Assert.Empty(await UnitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(1, 1)); } [Fact] @@ -436,7 +436,7 @@ public class CleanupServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder().Build()) .Build(); s.Library = new LibraryBuilder("Test LIb").Build(); - _context.Series.Add(s); + Context.Series.Add(s); var c = new AppUserCollection() { @@ -446,24 +446,24 @@ public class CleanupServiceTests : AbstractDbTest Items = new List() {s} }; - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007", Collections = new List() {c} }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); // Delete the Chapter - _context.Series.Remove(s); - await _unitOfWork.CommitAsync(); + Context.Series.Remove(s); + await UnitOfWork.CommitAsync(); await cleanupService.CleanupDbEntries(); - Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllCollectionsAsync()); + Assert.Empty(await UnitOfWork.CollectionTagRepository.GetAllCollectionsAsync()); } #endregion @@ -480,15 +480,15 @@ public class CleanupServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb").Build(); - _context.Series.Add(s); + Context.Series.Add(s); var user = new AppUser() { UserName = "CleanupWantToRead_ShouldRemoveFullyReadSeries", }; - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); // Add want to read user.WantToRead = new List() @@ -498,12 +498,12 @@ public class CleanupServiceTests : AbstractDbTest SeriesId = s.Id } }; - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); await _readerService.MarkSeriesAsRead(user, s.Id); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); - var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); @@ -511,7 +511,7 @@ public class CleanupServiceTests : AbstractDbTest await cleanupService.CleanupWantToRead(); var wantToRead = - await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(user.Id, new UserParams(), new FilterDto()); + await UnitOfWork.SeriesRepository.GetWantToReadForUserAsync(user.Id, new UserParams(), new FilterDto()); Assert.Equal(0, wantToRead.TotalCount); } @@ -533,15 +533,15 @@ public class CleanupServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test Lib").Build(); - _context.Series.Add(s); + Context.Series.Add(s); var user = new AppUser() { UserName = "ConsolidateProgress_ShouldRemoveDuplicates", }; - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); // Add 2 progress events user.Progresses ??= []; @@ -553,7 +553,7 @@ public class CleanupServiceTests : AbstractDbTest LibraryId = s.LibraryId, PagesRead = 1, }); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); // Add a duplicate with higher page number user.Progresses.Add(new AppUserProgress() @@ -564,18 +564,18 @@ public class CleanupServiceTests : AbstractDbTest LibraryId = s.LibraryId, PagesRead = 3, }); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); - Assert.Equal(2, (await _unitOfWork.AppUserProgressRepository.GetAllProgress()).Count()); + Assert.Equal(2, (await UnitOfWork.AppUserProgressRepository.GetAllProgress()).Count()); - var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); await cleanupService.ConsolidateProgress(); - var progress = await _unitOfWork.AppUserProgressRepository.GetAllProgress(); + var progress = await UnitOfWork.AppUserProgressRepository.GetAllProgress(); Assert.Single(progress); Assert.True(progress.First().PagesRead == 3); @@ -601,50 +601,50 @@ public class CleanupServiceTests : AbstractDbTest { new VolumeBuilder(API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume).WithChapter(c).Build() }; - _context.Series.Add(s); + Context.Series.Add(s); var user = new AppUser() { UserName = "EnsureChapterProgressIsCapped", Progresses = new List() }; - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); await _readerService.MarkChaptersAsRead(user, s.Id, new List() {c}); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); - var chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); - await _unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); + var chapter = await UnitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); + await UnitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); Assert.NotNull(chapter); Assert.Equal(2, chapter.PagesRead); // Update chapter to have 1 page c.Pages = 1; - _unitOfWork.ChapterRepository.Update(c); - await _unitOfWork.CommitAsync(); + UnitOfWork.ChapterRepository.Update(c); + await UnitOfWork.CommitAsync(); - chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); - await _unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); + chapter = await UnitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); + await UnitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); Assert.NotNull(chapter); Assert.Equal(2, chapter.PagesRead); Assert.Equal(1, chapter.Pages); - var cleanupService = new CleanupService(Substitute.For>(), _unitOfWork, + var cleanupService = new CleanupService(Substitute.For>(), UnitOfWork, Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem())); await cleanupService.EnsureChapterProgressIsCapped(); - chapter = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); - await _unitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); + chapter = await UnitOfWork.ChapterRepository.GetChapterDtoAsync(c.Id); + await UnitOfWork.ChapterRepository.AddChapterModifiers(user.Id, chapter); Assert.NotNull(chapter); Assert.Equal(1, chapter.PagesRead); - _context.AppUser.Remove(user); - await _unitOfWork.CommitAsync(); + Context.AppUser.Remove(user); + await UnitOfWork.CommitAsync(); } #endregion diff --git a/API.Tests/Services/CollectionTagServiceTests.cs b/API.Tests/Services/CollectionTagServiceTests.cs index 14ce131d8..3414dd86b 100644 --- a/API.Tests/Services/CollectionTagServiceTests.cs +++ b/API.Tests/Services/CollectionTagServiceTests.cs @@ -23,24 +23,24 @@ public class CollectionTagServiceTests : AbstractDbTest private readonly ICollectionTagService _service; public CollectionTagServiceTests() { - _service = new CollectionTagService(_unitOfWork, Substitute.For()); + _service = new CollectionTagService(UnitOfWork, Substitute.For()); } protected override async Task ResetDb() { - _context.AppUserCollection.RemoveRange(_context.AppUserCollection.ToList()); - _context.Library.RemoveRange(_context.Library.ToList()); + Context.AppUserCollection.RemoveRange(Context.AppUserCollection.ToList()); + Context.Library.RemoveRange(Context.Library.ToList()); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); } private async Task SeedSeries() { - if (_context.AppUserCollection.Any()) return; + if (Context.AppUserCollection.Any()) return; var s1 = new SeriesBuilder("Series 1").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.Mature).Build()).Build(); var s2 = new SeriesBuilder("Series 2").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.G).Build()).Build(); - _context.Library.Add(new LibraryBuilder("Library 2", LibraryType.Manga) + Context.Library.Add(new LibraryBuilder("Library 2", LibraryType.Manga) .WithSeries(s1) .WithSeries(s2) .Build()); @@ -51,9 +51,9 @@ public class CollectionTagServiceTests : AbstractDbTest new AppUserCollectionBuilder("Tag 1").WithItems(new []{s1}).Build(), new AppUserCollectionBuilder("Tag 2").WithItems(new []{s1, s2}).WithIsPromoted(true).Build() }; - _unitOfWork.UserRepository.Add(user); + UnitOfWork.UserRepository.Add(user); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); } #region DeleteTag @@ -64,7 +64,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Arrange await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Act @@ -72,7 +72,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Assert Assert.True(result); - var deletedTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var deletedTag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.Null(deletedTag); Assert.Single(user.Collections); // Only one collection should remain } @@ -82,7 +82,7 @@ public class CollectionTagServiceTests : AbstractDbTest { // Arrange await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Act - Try to delete a non-existent tag @@ -98,7 +98,7 @@ public class CollectionTagServiceTests : AbstractDbTest { // Arrange await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Act @@ -106,7 +106,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Assert Assert.True(result); - var remainingTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2); + var remainingTag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); Assert.NotNull(remainingTag); Assert.Equal("Tag 2", remainingTag.Title); Assert.True(remainingTag.Promoted); @@ -121,12 +121,12 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); user.Collections.Add(new AppUserCollectionBuilder("UpdateTag_ShouldUpdateFields").WithIsPromoted(true).Build()); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + UnitOfWork.UserRepository.Update(user); + await UnitOfWork.CommitAsync(); await _service.UpdateTag(new AppUserCollectionDto() { @@ -137,7 +137,7 @@ public class CollectionTagServiceTests : AbstractDbTest AgeRating = AgeRating.Unknown }, 1); - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(3); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(3); Assert.NotNull(tag); Assert.True(tag.Promoted); Assert.False(string.IsNullOrEmpty(tag.Summary)); @@ -151,12 +151,12 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); user.Collections.Add(new AppUserCollectionBuilder("UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource").WithSource(ScrobbleProvider.Mal).Build()); - _unitOfWork.UserRepository.Update(user); - await _unitOfWork.CommitAsync(); + UnitOfWork.UserRepository.Update(user); + await UnitOfWork.CommitAsync(); await _service.UpdateTag(new AppUserCollectionDto() { @@ -167,7 +167,7 @@ public class CollectionTagServiceTests : AbstractDbTest AgeRating = AgeRating.Unknown }, 1); - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(3); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(3); Assert.NotNull(tag); Assert.Equal("UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource", tag.Title); Assert.False(string.IsNullOrEmpty(tag.Summary)); @@ -198,8 +198,8 @@ public class CollectionTagServiceTests : AbstractDbTest // Create a second user var user2 = new AppUserBuilder("user2", "user2", Seed.DefaultThemes.First()).Build(); - _unitOfWork.UserRepository.Add(user2); - await _unitOfWork.CommitAsync(); + UnitOfWork.UserRepository.Add(user2); + await UnitOfWork.CommitAsync(); // Act & Assert var exception = await Assert.ThrowsAsync(() => _service.UpdateTag(new AppUserCollectionDto() @@ -261,7 +261,7 @@ public class CollectionTagServiceTests : AbstractDbTest }, 1); // Assert - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.True(tag.CoverImageLocked); @@ -273,7 +273,7 @@ public class CollectionTagServiceTests : AbstractDbTest CoverImageLocked = false }, 1); - tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.False(tag.CoverImageLocked); Assert.Equal(string.Empty, tag.CoverImage); @@ -286,7 +286,7 @@ public class CollectionTagServiceTests : AbstractDbTest await SeedSeries(); // Setup a user with admin role - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); await AddUserWithRole(user.Id, PolicyConstants.AdminRole); @@ -300,7 +300,7 @@ public class CollectionTagServiceTests : AbstractDbTest }, 1); // Assert - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.True(tag.Promoted); } @@ -312,7 +312,7 @@ public class CollectionTagServiceTests : AbstractDbTest await SeedSeries(); // Setup a user with promote role - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Mock to return promote role for the user @@ -327,7 +327,7 @@ public class CollectionTagServiceTests : AbstractDbTest }, 1); // Assert - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.True(tag.Promoted); } @@ -339,7 +339,7 @@ public class CollectionTagServiceTests : AbstractDbTest await SeedSeries(); // Setup a user with no special roles - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Act - Try to promote a tag without proper role @@ -351,7 +351,7 @@ public class CollectionTagServiceTests : AbstractDbTest }, 1); // Assert - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.False(tag.Promoted); // Should remain unpromoted } @@ -365,15 +365,15 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Tag 2 has 2 series - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); Assert.NotNull(tag); await _service.RemoveTagFromSeries(tag, new[] {1}); - var userCollections = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var userCollections = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.Equal(2, userCollections!.Collections.Count); Assert.Single(tag.Items); Assert.Equal(2, tag.Items.First().Id); @@ -387,11 +387,11 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Tag 2 has 2 series - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); Assert.NotNull(tag); await _service.RemoveTagFromSeries(tag, new[] {1}); @@ -407,15 +407,15 @@ public class CollectionTagServiceTests : AbstractDbTest { await SeedSeries(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections); Assert.NotNull(user); // Tag 1 has 1 series - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); await _service.RemoveTagFromSeries(tag, new[] {1}); - var tag2 = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag2 = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.Null(tag2); } @@ -435,7 +435,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Arrange await SeedSeries(); - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); var initialItemCount = tag.Items.Count; @@ -444,7 +444,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Assert Assert.True(result); - tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.Equal(initialItemCount, tag.Items.Count); // No items should be removed } @@ -455,7 +455,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Arrange await SeedSeries(); - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); var initialItemCount = tag.Items.Count; @@ -464,7 +464,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Assert Assert.True(result); - tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); Assert.Equal(initialItemCount, tag.Items.Count); // No items should be removed } @@ -475,13 +475,13 @@ public class CollectionTagServiceTests : AbstractDbTest // Arrange await SeedSeries(); - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.NotNull(tag); // Force null items list tag.Items = null; - _unitOfWork.CollectionTagRepository.Update(tag); - await _unitOfWork.CommitAsync(); + UnitOfWork.CollectionTagRepository.Update(tag); + await UnitOfWork.CommitAsync(); // Act var result = await _service.RemoveTagFromSeries(tag, [1]); @@ -489,7 +489,7 @@ public class CollectionTagServiceTests : AbstractDbTest // Assert Assert.True(result); // The tag should not be removed since the items list was null, not empty - var tagAfter = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1); + var tagAfter = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(1); Assert.Null(tagAfter); } @@ -501,21 +501,21 @@ public class CollectionTagServiceTests : AbstractDbTest // Add a third series with a different age rating var s3 = new SeriesBuilder("Series 3").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.PG).Build()).Build(); - _context.Library.First().Series.Add(s3); - await _unitOfWork.CommitAsync(); + Context.Library.First().Series.Add(s3); + await UnitOfWork.CommitAsync(); // Add series 3 to tag 2 - var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2); + var tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); Assert.NotNull(tag); tag.Items.Add(s3); - _unitOfWork.CollectionTagRepository.Update(tag); - await _unitOfWork.CommitAsync(); + UnitOfWork.CollectionTagRepository.Update(tag); + await UnitOfWork.CommitAsync(); // Act - Remove the series with Mature rating await _service.RemoveTagFromSeries(tag, new[] {1}); // Assert - tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2); + tag = await UnitOfWork.CollectionTagRepository.GetCollectionAsync(2); Assert.NotNull(tag); Assert.Equal(2, tag.Items.Count); diff --git a/API.Tests/Services/CoverDbServiceTests.cs b/API.Tests/Services/CoverDbServiceTests.cs new file mode 100644 index 000000000..93217c3b5 --- /dev/null +++ b/API.Tests/Services/CoverDbServiceTests.cs @@ -0,0 +1,117 @@ +using System.IO; +using System.IO.Abstractions; +using System.Reflection; +using System.Threading.Tasks; +using API.Constants; +using API.Entities.Enums; +using API.Extensions; +using API.Services; +using API.Services.Tasks.Metadata; +using API.SignalR; +using EasyCaching.Core; +using Kavita.Common; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace API.Tests.Services; + +public class CoverDbServiceTests : AbstractDbTest +{ + private readonly DirectoryService _directoryService; + private readonly IEasyCachingProviderFactory _cacheFactory = Substitute.For(); + private readonly ICoverDbService _coverDbService; + + private static readonly string FaviconPath = Path.Join(Directory.GetCurrentDirectory(), + "../../../Services/Test Data/CoverDbService/Favicons"); + /// + /// Path to download files temp to. Should be empty after each test. + /// + private static readonly string TempPath = Path.Join(Directory.GetCurrentDirectory(), + "../../../Services/Test Data/CoverDbService/Temp"); + + public CoverDbServiceTests() + { + _directoryService = new DirectoryService(Substitute.For>(), CreateFileSystem()); + var imageService = new ImageService(Substitute.For>(), _directoryService); + + _coverDbService = new CoverDbService(Substitute.For>(), _directoryService, _cacheFactory, + Substitute.For(), imageService, UnitOfWork, Substitute.For()); + } + + protected override Task ResetDb() + { + throw new System.NotImplementedException(); + } + + + #region Download Favicon + + /// + /// I cannot figure out how to test this code due to the reliance on the _directoryService.FaviconDirectory and not being + /// able to redirect it to the real filesystem. + /// + public async Task DownloadFaviconAsync_ShouldDownloadAndMatchExpectedFavicon() + { + // Arrange + var testUrl = "https://anilist.co/anime/6205/Kmpfer/"; + var encodeFormat = EncodeFormat.WEBP; + var expectedFaviconPath = Path.Combine(FaviconPath, "anilist.co.webp"); + + // Ensure TempPath exists + _directoryService.ExistOrCreate(TempPath); + + var baseUrl = "https://anilist.co"; + + // Ensure there is no cache result for this URL + var provider = Substitute.For(); + provider.GetAsync(baseUrl).Returns(new CacheValue(null, false)); + _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon).Returns(provider); + + + // // Replace favicon directory with TempPath + // var directoryService = (DirectoryService)_directoryService; + // directoryService.FaviconDirectory = TempPath; + + // Hack: Swap FaviconDirectory with TempPath for ability to download real files + typeof(DirectoryService) + .GetField("FaviconDirectory", BindingFlags.NonPublic | BindingFlags.Instance) + ?.SetValue(_directoryService, TempPath); + + + // Act + var resultFilename = await _coverDbService.DownloadFaviconAsync(testUrl, encodeFormat); + var actualFaviconPath = Path.Combine(TempPath, resultFilename); + + // Assert file exists + Assert.True(File.Exists(actualFaviconPath), "Downloaded favicon does not exist in temp path"); + + // Load and compare similarity + + var similarity = expectedFaviconPath.CalculateSimilarity(actualFaviconPath); // Assuming you have this extension + Assert.True(similarity > 0.9f, $"Image similarity too low: {similarity}"); + } + + [Fact] + public async Task DownloadFaviconAsync_ShouldThrowKavitaException_WhenPreviouslyFailedUrlExistsInCache() + { + // Arrange + var testUrl = "https://example.com"; + var encodeFormat = EncodeFormat.WEBP; + + var provider = Substitute.For(); + provider.GetAsync(Arg.Any()) + .Returns(new CacheValue(string.Empty, true)); // Simulate previous failure + + _cacheFactory.GetCachingProvider(EasyCacheProfiles.Favicon).Returns(provider); + + // Act & Assert + await Assert.ThrowsAsync(() => + _coverDbService.DownloadFaviconAsync(testUrl, encodeFormat)); + } + + #endregion + + +} diff --git a/API.Tests/Services/DeviceServiceTests.cs b/API.Tests/Services/DeviceServiceTests.cs index 1d021c76d..cbcf70f82 100644 --- a/API.Tests/Services/DeviceServiceTests.cs +++ b/API.Tests/Services/DeviceServiceTests.cs @@ -18,13 +18,13 @@ public class DeviceServiceDbTests : AbstractDbTest public DeviceServiceDbTests() : base() { - _deviceService = new DeviceService(_unitOfWork, _logger, Substitute.For()); + _deviceService = new DeviceService(UnitOfWork, _logger, Substitute.For()); } protected override async Task ResetDb() { - _context.Users.RemoveRange(_context.Users.ToList()); - await _unitOfWork.CommitAsync(); + Context.Users.RemoveRange(Context.Users.ToList()); + await UnitOfWork.CommitAsync(); } @@ -39,8 +39,8 @@ public class DeviceServiceDbTests : AbstractDbTest Devices = new List() }; - _context.Users.Add(user); - await _unitOfWork.CommitAsync(); + Context.Users.Add(user); + await UnitOfWork.CommitAsync(); var device = await _deviceService.Create(new CreateDeviceDto() { @@ -62,8 +62,8 @@ public class DeviceServiceDbTests : AbstractDbTest Devices = new List() }; - _context.Users.Add(user); - await _unitOfWork.CommitAsync(); + Context.Users.Add(user); + await UnitOfWork.CommitAsync(); var device = await _deviceService.Create(new CreateDeviceDto() { diff --git a/API.Tests/Services/ExternalMetadataServiceTests.cs b/API.Tests/Services/ExternalMetadataServiceTests.cs index 127bceb7a..8310ed269 100644 --- a/API.Tests/Services/ExternalMetadataServiceTests.cs +++ b/API.Tests/Services/ExternalMetadataServiceTests.cs @@ -40,8 +40,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest // Set up Hangfire to use in-memory storage for testing GlobalConfiguration.Configuration.UseInMemoryStorage(); - _externalMetadataService = new ExternalMetadataService(_unitOfWork, Substitute.For>(), - _mapper, Substitute.For(), Substitute.For(), Substitute.For(), + _externalMetadataService = new ExternalMetadataService(UnitOfWork, Substitute.For>(), + Mapper, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For()); } @@ -58,14 +58,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = false; metadataSettings.EnableSummary = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -75,7 +75,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(string.Empty, postSeries.Metadata.Summary); } @@ -95,14 +95,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableSummary = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -112,7 +112,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(string.Empty, postSeries.Metadata.Summary); } @@ -128,14 +128,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableSummary = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -145,7 +145,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); Assert.Equal(series.Metadata.Summary, postSeries.Metadata.Summary); @@ -163,14 +163,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithSummary("This summary is not locked") .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableSummary = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -180,7 +180,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); Assert.Equal("This summary is not locked", postSeries.Metadata.Summary); @@ -198,14 +198,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithSummary("This summary is not locked", true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableSummary = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -215,7 +215,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); Assert.Equal("This summary is not locked", postSeries.Metadata.Summary); @@ -233,15 +233,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithSummary("This summary is not locked", true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableSummary = true; metadataSettings.Overrides = [MetadataSettingField.Summary]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -251,7 +251,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.False(string.IsNullOrEmpty(postSeries.Metadata.Summary)); Assert.Equal("This should write", postSeries.Metadata.Summary); @@ -273,14 +273,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableStartDate = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -290,7 +290,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(0, postSeries.Metadata.ReleaseYear); } @@ -306,14 +306,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableStartDate = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -323,7 +323,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(DateTime.UtcNow.Year, postSeries.Metadata.ReleaseYear); } @@ -340,14 +340,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithReleaseYear(1990) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableStartDate = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -357,7 +357,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(1990, postSeries.Metadata.ReleaseYear); } @@ -374,14 +374,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithReleaseYear(1990, true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableStartDate = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -391,7 +391,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(1990, postSeries.Metadata.ReleaseYear); } @@ -408,15 +408,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithReleaseYear(1990, true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableStartDate = true; metadataSettings.Overrides = [MetadataSettingField.StartDate]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -426,7 +426,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(DateTime.UtcNow.Year, postSeries.Metadata.ReleaseYear); } @@ -447,14 +447,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableLocalizedName = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -464,7 +464,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(string.Empty, postSeries.LocalizedName); } @@ -481,14 +481,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableLocalizedName = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -498,7 +498,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal("Kimchi", postSeries.LocalizedName); } @@ -515,14 +515,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableLocalizedName = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -532,7 +532,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal("Localized Name here", postSeries.LocalizedName); } @@ -549,14 +549,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableLocalizedName = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -566,7 +566,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal("Localized Name here", postSeries.LocalizedName); } @@ -583,15 +583,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableLocalizedName = true; metadataSettings.Overrides = [MetadataSettingField.LocalizedName]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -601,7 +601,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal("Kimchi", postSeries.LocalizedName); } @@ -618,14 +618,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableLocalizedName = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -635,7 +635,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.True(string.IsNullOrEmpty(postSeries.LocalizedName)); } @@ -661,14 +661,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePublicationStatus = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -678,7 +678,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(PublicationStatus.OnGoing, postSeries.Metadata.PublicationStatus); } @@ -700,14 +700,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePublicationStatus = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -717,7 +717,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(PublicationStatus.Completed, postSeries.Metadata.PublicationStatus); } @@ -740,14 +740,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePublicationStatus = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -757,7 +757,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(PublicationStatus.Completed, postSeries.Metadata.PublicationStatus); } @@ -780,14 +780,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePublicationStatus = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -797,7 +797,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(PublicationStatus.Hiatus, postSeries.Metadata.PublicationStatus); } @@ -820,15 +820,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithChapter(new ChapterBuilder("2").Build()) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePublicationStatus = true; metadataSettings.Overrides = [MetadataSettingField.PublicationStatus]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -838,7 +838,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(PublicationStatus.Completed, postSeries.Metadata.PublicationStatus); } @@ -858,14 +858,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithChapter(new ChapterBuilder("1").Build()) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePublicationStatus = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -875,7 +875,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(PublicationStatus.Ended, postSeries.Metadata.PublicationStatus); } @@ -897,18 +897,18 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.AgeRatingMappings = new Dictionary() { {"Ecchi", AgeRating.Teen}, // Genre {"H", AgeRating.R18Plus}, // Tag }; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -918,7 +918,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(AgeRating.Teen, postSeries.Metadata.AgeRating); } @@ -935,18 +935,18 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithAgeRating(AgeRating.Mature) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.AgeRatingMappings = new Dictionary() { {"Ecchi", AgeRating.Teen}, // Genre {"H", AgeRating.R18Plus}, // Tag }; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -956,7 +956,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(AgeRating.Mature, postSeries.Metadata.AgeRating); } @@ -973,18 +973,18 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithAgeRating(AgeRating.Everyone) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.AgeRatingMappings = new Dictionary() { {"Ecchi", AgeRating.Teen}, // Genre {"H", AgeRating.R18Plus}, // Tag }; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -994,7 +994,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(AgeRating.Teen, postSeries.Metadata.AgeRating); } @@ -1011,18 +1011,18 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithAgeRating(AgeRating.Everyone, true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.AgeRatingMappings = new Dictionary() { {"Ecchi", AgeRating.Teen}, // Genre {"H", AgeRating.R18Plus}, // Tag }; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1032,7 +1032,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(AgeRating.Everyone, postSeries.Metadata.AgeRating); } @@ -1049,10 +1049,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithAgeRating(AgeRating.Everyone, true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.Overrides = [MetadataSettingField.AgeRating]; metadataSettings.AgeRatingMappings = new Dictionary() @@ -1060,8 +1060,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest {"Ecchi", AgeRating.Teen}, // Genre {"H", AgeRating.R18Plus}, // Tag }; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1071,7 +1071,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(AgeRating.Teen, postSeries.Metadata.AgeRating); } @@ -1091,14 +1091,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1108,7 +1108,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal([], postSeries.Metadata.Genres); } @@ -1124,14 +1124,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1141,7 +1141,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Ecchi"], postSeries.Metadata.Genres.Select(g => g.Title)); } @@ -1158,14 +1158,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithGenre(_genreLookup["Action"]) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1175,7 +1175,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Ecchi"], postSeries.Metadata.Genres.Select(g => g.Title)); } @@ -1192,14 +1192,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithGenre(_genreLookup["Action"], true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1209,7 +1209,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Action"], postSeries.Metadata.Genres.Select(g => g.Title)); } @@ -1226,15 +1226,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithGenre(_genreLookup["Action"], true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; metadataSettings.Overrides = [MetadataSettingField.Genres]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1244,7 +1244,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Ecchi"], postSeries.Metadata.Genres.Select(g => g.Title)); } @@ -1264,14 +1264,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1281,7 +1281,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal([], postSeries.Metadata.Tags); } @@ -1297,14 +1297,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1314,7 +1314,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Boxing"], postSeries.Metadata.Tags.Select(t => t.Title)); } @@ -1331,14 +1331,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithTag(_tagLookup["H"], true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1348,7 +1348,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["H"], postSeries.Metadata.Tags.Select(t => t.Title)); } @@ -1365,15 +1365,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithTag(_tagLookup["H"], true) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.Overrides = [MetadataSettingField.Tags]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1383,7 +1383,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Boxing"], postSeries.Metadata.Tags.Select(t => t.Title)); } @@ -1403,14 +1403,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1420,7 +1420,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal([], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer)); } @@ -1436,15 +1436,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1454,7 +1454,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["John Doe"], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).Select(p => p.Person.Name)); } @@ -1472,15 +1472,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1490,7 +1490,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) @@ -1511,17 +1511,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = [PersonRole.Writer]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1531,7 +1531,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"John Doe", "Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) @@ -1554,17 +1554,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = false; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = [PersonRole.Writer]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1574,7 +1574,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) @@ -1595,17 +1595,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = []; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1615,7 +1615,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) @@ -1635,17 +1635,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = [PersonRole.Writer]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1655,7 +1655,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"John Doe"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) @@ -1668,7 +1668,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest Staff = [CreateStaff("John", "Doe 2", "Story")] }, 1); - postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"John Doe 2"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer) @@ -1678,6 +1678,130 @@ public class ExternalMetadataServiceTests : AbstractDbTest #endregion + #region People Alias + + [Fact] + public async Task PeopleAliasing_AddAsAlias() + { + await ResetDb(); + + const string seriesName = "Test - People - Add as Alias"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .Build()) + .Build(); + Context.Series.Attach(series); + Context.Person.Add(new PersonBuilder("John Doe").Build()); + await Context.SaveChangesAsync(); + + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); + metadataSettings.Enabled = true; + metadataSettings.EnablePeople = true; + metadataSettings.FirstLastPeopleNaming = true; + metadataSettings.Overrides = [MetadataSettingField.People]; + metadataSettings.PersonRoles = [PersonRole.Writer]; + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); + + await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() + { + Name = seriesName, + Staff = [CreateStaff("Doe", "John", "Story")] + }, 1); + + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + Assert.NotNull(postSeries); + + var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); + Assert.Single(allWriters); + + var johnDoe = allWriters[0].Person; + + Assert.Contains("Doe John", johnDoe.Aliases.Select(pa => pa.Alias)); + } + + [Fact] + public async Task PeopleAliasing_AddOnAlias() + { + await ResetDb(); + + const string seriesName = "Test - People - Add as Alias"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .Build()) + .Build(); + Context.Series.Attach(series); + + Context.Person.Add(new PersonBuilder("John Doe").WithAlias("Doe John").Build()); + + await Context.SaveChangesAsync(); + + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); + metadataSettings.Enabled = true; + metadataSettings.EnablePeople = true; + metadataSettings.FirstLastPeopleNaming = true; + metadataSettings.Overrides = [MetadataSettingField.People]; + metadataSettings.PersonRoles = [PersonRole.Writer]; + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); + + await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() + { + Name = seriesName, + Staff = [CreateStaff("Doe", "John", "Story")] + }, 1); + + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + Assert.NotNull(postSeries); + + var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); + Assert.Single(allWriters); + + var johnDoe = allWriters[0].Person; + + Assert.Contains("Doe John", johnDoe.Aliases.Select(pa => pa.Alias)); + } + + [Fact] + public async Task PeopleAliasing_DontAddAsAlias_SameButNotSwitched() + { + await ResetDb(); + + const string seriesName = "Test - People - Add as Alias"; + var series = new SeriesBuilder(seriesName) + .WithLibraryId(1) + .WithMetadata(new SeriesMetadataBuilder() + .Build()) + .Build(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); + + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); + metadataSettings.Enabled = true; + metadataSettings.EnablePeople = true; + metadataSettings.FirstLastPeopleNaming = true; + metadataSettings.Overrides = [MetadataSettingField.People]; + metadataSettings.PersonRoles = [PersonRole.Writer]; + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); + + await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() + { + Name = seriesName, + Staff = [CreateStaff("John", "Doe Doe", "Story"), CreateStaff("Doe", "John Doe", "Story")] + }, 1); + + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + Assert.NotNull(postSeries); + + var allWriters = postSeries.Metadata.People.Where(p => p.Role == PersonRole.Writer).ToList(); + Assert.Equal(2, allWriters.Count); + } + + #endregion + #region People - Characters [Fact] @@ -1691,14 +1815,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = false; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1708,7 +1832,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal([], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character)); } @@ -1724,15 +1848,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1742,7 +1866,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["John Doe"], postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character).Select(p => p.Person.Name)); } @@ -1760,15 +1884,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.CharacterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1778,7 +1902,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) @@ -1799,17 +1923,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = [PersonRole.Character]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1819,7 +1943,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"John Doe", "Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) @@ -1842,17 +1966,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = false; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = [PersonRole.Character]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1862,7 +1986,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"Johnny Twowheeler", "Twowheeler Johnny"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) @@ -1883,17 +2007,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .Build()) .Build(); series.Metadata.WriterLocked = true; - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = []; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1903,7 +2027,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"Johnny Twowheeler"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) @@ -1923,17 +2047,17 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnablePeople = true; metadataSettings.FirstLastPeopleNaming = true; metadataSettings.Overrides = [MetadataSettingField.People]; metadataSettings.PersonRoles = [PersonRole.Character]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -1943,7 +2067,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"John Doe"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) @@ -1956,7 +2080,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest Characters = [CreateCharacter("John", "Doe 2", CharacterRole.Main)] }, 1); - postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[]{"John Doe 2"}.OrderBy(s => s), postSeries.Metadata.People.Where(p => p.Role == PersonRole.Character) @@ -1988,7 +2112,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); + Context.Series.Attach(series); var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") .WithLibraryId(1) @@ -2000,14 +2124,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest AniListId = 10 }) .Build(); - _context.Series.Attach(series2); - await _context.SaveChangesAsync(); + Context.Series.Attach(series2); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableRelationships = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() { @@ -2028,7 +2152,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); + var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(sourceSeries); Assert.Single(sourceSeries.Relations); Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name); @@ -2046,7 +2170,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); + Context.Series.Attach(series); var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") .WithLibraryId(1) @@ -2055,14 +2179,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series2); - await _context.SaveChangesAsync(); + Context.Series.Attach(series2); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableRelationships = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() { @@ -2083,7 +2207,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); + var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(sourceSeries); Assert.Single(sourceSeries.Relations); Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name); @@ -2102,7 +2226,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); + Context.Series.Attach(series); var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") .WithLibraryId(1) @@ -2111,14 +2235,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series2); - await _context.SaveChangesAsync(); + Context.Series.Attach(series2); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableRelationships = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() { @@ -2139,7 +2263,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); + var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(sourceSeries); Assert.Empty(sourceSeries.Relations); } @@ -2153,8 +2277,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest var existingRelationshipSeries = new SeriesBuilder("Existing") .WithLibraryId(1) .Build(); - _context.Series.Attach(existingRelationshipSeries); - await _context.SaveChangesAsync(); + Context.Series.Attach(existingRelationshipSeries); + await Context.SaveChangesAsync(); const string seriesName = "Test - Relationships Side Story"; var series = new SeriesBuilder(seriesName) @@ -2164,7 +2288,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); + Context.Series.Attach(series); var series2 = new SeriesBuilder("Test - Relationships Side Story - Target") .WithLibraryId(1) @@ -2176,14 +2300,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest AniListId = 10 }) .Build(); - _context.Series.Attach(series2); - await _context.SaveChangesAsync(); + Context.Series.Attach(series2); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableRelationships = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2204,7 +2328,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 2); // Repull Series and validate what is overwritten - var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); + var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(sourceSeries); Assert.Equal(seriesName, sourceSeries.Name); @@ -2227,7 +2351,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); + Context.Series.Attach(series); var series2 = new SeriesBuilder("Test - Relationships Target") .WithLibraryId(1) @@ -2235,14 +2359,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series2); - await _context.SaveChangesAsync(); + Context.Series.Attach(series2); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableRelationships = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() { @@ -2262,12 +2386,12 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var sourceSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); + var sourceSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(sourceSeries); Assert.Single(sourceSeries.Relations); Assert.Equal(series2.Name, sourceSeries.Relations.First().TargetSeries.Name); - var sequel = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); + var sequel = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(sequel); Assert.Equal(seriesName, sequel.Relations.First().TargetSeries.Name); } @@ -2284,7 +2408,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); + Context.Series.Attach(series); // ID 2: Blue Lock var series2 = new SeriesBuilder("Blue Lock") @@ -2293,14 +2417,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series2); - await _context.SaveChangesAsync(); + Context.Series.Attach(series2); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableRelationships = true; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); // Apply to Blue Lock - Episode Nagi (ID 1), setting Blue Lock (ID 2) as its prequel await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2324,14 +2448,14 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Apply to series ID 1 (Nagi) // Verify Blue Lock - Episode Nagi has Blue Lock as prequel - var nagiSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); + var nagiSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(nagiSeries); Assert.Single(nagiSeries.Relations); Assert.Equal("Blue Lock", nagiSeries.Relations.First().TargetSeries.Name); Assert.Equal(RelationKind.Prequel, nagiSeries.Relations.First().RelationKind); // Verify Blue Lock has Blue Lock - Episode Nagi as sequel - var blueLockSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); + var blueLockSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Metadata | SeriesIncludes.Related); Assert.NotNull(blueLockSeries); Assert.Single(blueLockSeries.Relations); Assert.Equal("Blue Lock - Episode Nagi", blueLockSeries.Relations.First().TargetSeries.Name); @@ -2354,16 +2478,16 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.EnableGenres = true; metadataSettings.Blacklist = ["Sports", "Action"]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2373,7 +2497,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[] {"Boxing"}.OrderBy(s => s), postSeries.Metadata.Genres.Select(t => t.Title).OrderBy(s => s)); } @@ -2390,16 +2514,16 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.EnableGenres = true; metadataSettings.Blacklist = ["Sports", "Action"]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2409,7 +2533,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[] {"Boxing"}.OrderBy(s => s), postSeries.Metadata.Tags.Select(t => t.Title).OrderBy(s => s)); } @@ -2435,15 +2559,15 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.Whitelist = ["Sports", "Action"]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2453,7 +2577,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[] {"Sports", "Action"}.OrderBy(s => s), postSeries.Metadata.Tags.Select(t => t.Title).OrderBy(s => s)); } @@ -2469,10 +2593,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.FieldMappings = [new MetadataFieldMapping() @@ -2485,8 +2609,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; metadataSettings.Whitelist = ["Sports", "Action"]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2496,7 +2620,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(new[] {"Sports", "Action"}.OrderBy(s => s), postSeries.Metadata.Tags.Select(t => t.Title).OrderBy(s => s)); } @@ -2516,10 +2640,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; metadataSettings.Overrides = [MetadataSettingField.Genres]; @@ -2533,8 +2657,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2544,7 +2668,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal( new[] { "Ecchi", "Fanservice" }.OrderBy(s => s), @@ -2563,10 +2687,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; metadataSettings.Overrides = [MetadataSettingField.Genres]; @@ -2580,8 +2704,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2591,7 +2715,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Fanservice"], postSeries.Metadata.Genres.Select(g => g.Title)); } @@ -2607,10 +2731,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.FieldMappings = [new MetadataFieldMapping() @@ -2623,8 +2747,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2634,7 +2758,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal( new[] { "Ecchi", "Fanservice" }.OrderBy(s => s), @@ -2653,10 +2777,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableTags = true; metadataSettings.Overrides = [MetadataSettingField.Genres]; @@ -2670,8 +2794,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2681,7 +2805,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal(["Fanservice"], postSeries.Metadata.Tags.Select(g => g.Title)); } @@ -2697,10 +2821,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder() .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; metadataSettings.EnableTags = true; @@ -2715,8 +2839,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2726,7 +2850,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal( new[] {"Ecchi"}.OrderBy(s => s), @@ -2752,10 +2876,10 @@ public class ExternalMetadataServiceTests : AbstractDbTest .WithGenre(_genreLookup["Action"]) .Build()) .Build(); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = true; metadataSettings.EnableGenres = true; metadataSettings.EnableTags = true; @@ -2770,8 +2894,8 @@ public class ExternalMetadataServiceTests : AbstractDbTest }]; - _context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + Context.MetadataSettings.Update(metadataSettings); + await Context.SaveChangesAsync(); await _externalMetadataService.WriteExternalMetadataToSeries(new ExternalSeriesDetailDto() @@ -2780,7 +2904,7 @@ public class ExternalMetadataServiceTests : AbstractDbTest }, 1); // Repull Series and validate what is overwritten - var postSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); + var postSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Metadata); Assert.NotNull(postSeries); Assert.Equal( new[] {"Action"}.OrderBy(s => s), @@ -2794,13 +2918,13 @@ public class ExternalMetadataServiceTests : AbstractDbTest protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series); - _context.AppUser.RemoveRange(_context.AppUser); - _context.Genre.RemoveRange(_context.Genre); - _context.Tag.RemoveRange(_context.Tag); - _context.Person.RemoveRange(_context.Person); + Context.Series.RemoveRange(Context.Series); + Context.AppUser.RemoveRange(Context.AppUser); + Context.Genre.RemoveRange(Context.Genre); + Context.Tag.RemoveRange(Context.Tag); + Context.Person.RemoveRange(Context.Person); - var metadataSettings = await _unitOfWork.SettingsRepository.GetMetadataSettings(); + var metadataSettings = await UnitOfWork.SettingsRepository.GetMetadataSettings(); metadataSettings.Enabled = false; metadataSettings.EnableSummary = false; metadataSettings.EnableCoverImage = false; @@ -2811,41 +2935,41 @@ public class ExternalMetadataServiceTests : AbstractDbTest metadataSettings.EnableTags = false; metadataSettings.EnablePublicationStatus = false; metadataSettings.EnableStartDate = false; - _context.MetadataSettings.Update(metadataSettings); + Context.MetadataSettings.Update(metadataSettings); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - _context.AppUser.Add(new AppUserBuilder("Joe", "Joe") + Context.AppUser.Add(new AppUserBuilder("Joe", "Joe") .WithRole(PolicyConstants.AdminRole) - .WithLibrary(await _context.Library.FirstAsync(l => l.Id == 1)) + .WithLibrary(await Context.Library.FirstAsync(l => l.Id == 1)) .Build()); // Create a bunch of Genres for this test and store their string in _genreLookup _genreLookup.Clear(); var g1 = new GenreBuilder("Action").Build(); var g2 = new GenreBuilder("Ecchi").Build(); - _context.Genre.Add(g1); - _context.Genre.Add(g2); + Context.Genre.Add(g1); + Context.Genre.Add(g2); _genreLookup.Add("Action", g1); _genreLookup.Add("Ecchi", g2); _tagLookup.Clear(); var t1 = new TagBuilder("H").Build(); var t2 = new TagBuilder("Boxing").Build(); - _context.Tag.Add(t1); - _context.Tag.Add(t2); + Context.Tag.Add(t1); + Context.Tag.Add(t2); _tagLookup.Add("H", t1); _tagLookup.Add("Boxing", t2); _personLookup.Clear(); var p1 = new PersonBuilder("Johnny Twowheeler").Build(); var p2 = new PersonBuilder("Boxing").Build(); - _context.Person.Add(p1); - _context.Person.Add(p2); + Context.Person.Add(p1); + Context.Person.Add(p2); _personLookup.Add("Johnny Twowheeler", p1); _personLookup.Add("Batman Robin", p2); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } private static SeriesStaffDto CreateStaff(string first, string last, string role) diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index f81ebd3c4..f8714f69a 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -99,14 +99,14 @@ public class ParseScannedFilesTests : AbstractDbTest { // Since ProcessFile relies on _readingItemService, we can implement our own versions of _readingItemService so we have control over how the calls work GlobalConfiguration.Configuration.UseInMemoryStorage(); - _scannerHelper = new ScannerHelper(_unitOfWork, testOutputHelper); + _scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper); } protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } #region MergeName @@ -206,13 +206,13 @@ public class ParseScannedFilesTests : AbstractDbTest var library = - await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Folders | LibraryIncludes.FileTypes); Assert.NotNull(library); library.Type = LibraryType.Manga; var parsedSeries = await psf.ScanLibrariesForSeries(library, new List() {Root + "Data/"}, false, - await _unitOfWork.SeriesRepository.GetFolderPathMap(1)); + await UnitOfWork.SeriesRepository.GetFolderPathMap(1)); // Assert.Equal(3, parsedSeries.Values.Count); @@ -251,9 +251,9 @@ public class ParseScannedFilesTests : AbstractDbTest new MockReadingItemService(ds, Substitute.For()), Substitute.For()); var directoriesSeen = new HashSet(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Folders | LibraryIncludes.FileTypes); - var scanResults = await psf.ScanFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library); + var scanResults = await psf.ScanFiles("C:/Data/", true, await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); foreach (var scanResult in scanResults) { directoriesSeen.Add(scanResult.Folder); @@ -270,13 +270,13 @@ public class ParseScannedFilesTests : AbstractDbTest var psf = new ParseScannedFiles(Substitute.For>(), ds, new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Folders | LibraryIncludes.FileTypes); Assert.NotNull(library); var directoriesSeen = new HashSet(); var scanResults = await psf.ScanFiles("C:/Data/", false, - await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library); + await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); foreach (var scanResult in scanResults) { @@ -305,10 +305,10 @@ public class ParseScannedFilesTests : AbstractDbTest var psf = new ParseScannedFiles(Substitute.For>(), ds, new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Folders | LibraryIncludes.FileTypes); Assert.NotNull(library); - var scanResults = await psf.ScanFiles("C:/Data", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library); + var scanResults = await psf.ScanFiles("C:/Data", true, await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); Assert.Equal(2, scanResults.Count); } @@ -334,11 +334,11 @@ public class ParseScannedFilesTests : AbstractDbTest var psf = new ParseScannedFiles(Substitute.For>(), ds, new MockReadingItemService(ds, Substitute.For()), Substitute.For()); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1, LibraryIncludes.Folders | LibraryIncludes.FileTypes); Assert.NotNull(library); var scanResults = await psf.ScanFiles("C:/Data", false, - await _unitOfWork.SeriesRepository.GetFolderPathMap(1), library); + await UnitOfWork.SeriesRepository.GetFolderPathMap(1), library); Assert.Single(scanResults); } @@ -357,8 +357,8 @@ public class ParseScannedFilesTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var fs = new FileSystem(); var ds = new DirectoryService(Substitute.For>(), fs); @@ -368,7 +368,7 @@ public class ParseScannedFilesTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(ds, fs); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(4, postLib.Series.Count); @@ -391,7 +391,7 @@ public class ParseScannedFilesTests : AbstractDbTest Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1 Ch. 0002.cbz")); // 4 series, of which 2 have volumes as directories - var folderMap = await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id); + var folderMap = await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id); Assert.Equal(6, folderMap.Count); var res = await psf.ScanFiles(testDirectoryPath, true, folderMap, postLib); @@ -409,8 +409,8 @@ public class ParseScannedFilesTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var fs = new FileSystem(); var ds = new DirectoryService(Substitute.For>(), fs); @@ -420,7 +420,7 @@ public class ParseScannedFilesTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(ds, fs); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(4, postLib.Series.Count); @@ -438,7 +438,7 @@ public class ParseScannedFilesTests : AbstractDbTest Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 2.cbz")); var res = await psf.ScanFiles(testDirectoryPath, true, - await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); + await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); var changes = res.Count(sc => sc.HasChanged); Assert.Equal(1, changes); } @@ -452,8 +452,8 @@ public class ParseScannedFilesTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var fs = new FileSystem(); var ds = new DirectoryService(Substitute.For>(), fs); @@ -463,7 +463,7 @@ public class ParseScannedFilesTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(ds, fs); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -476,7 +476,7 @@ public class ParseScannedFilesTests : AbstractDbTest await Task.Delay(1100); // Ensure at least one second has passed since library scan var res = await psf.ScanFiles(testDirectoryPath, true, - await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); + await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); Assert.DoesNotContain(res, sc => sc.HasChanged); } @@ -488,8 +488,8 @@ public class ParseScannedFilesTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var fs = new FileSystem(); var ds = new DirectoryService(Substitute.For>(), fs); @@ -499,7 +499,7 @@ public class ParseScannedFilesTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(ds, fs); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -508,8 +508,8 @@ public class ParseScannedFilesTests : AbstractDbTest Assert.Equal(4, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); spiceAndWolf.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(2)); - _context.Series.Update(spiceAndWolf); - await _context.SaveChangesAsync(); + Context.Series.Update(spiceAndWolf); + await Context.SaveChangesAsync(); // Add file at series root var spiceAndWolfDir = Path.Join(testDirectoryPath, "Spice and Wolf"); @@ -517,7 +517,7 @@ public class ParseScannedFilesTests : AbstractDbTest Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 4.cbz")); var res = await psf.ScanFiles(testDirectoryPath, true, - await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); + await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); var changes = res.Count(sc => sc.HasChanged); Assert.Equal(2, changes); } @@ -530,8 +530,8 @@ public class ParseScannedFilesTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var fs = new FileSystem(); var ds = new DirectoryService(Substitute.For>(), fs); @@ -541,7 +541,7 @@ public class ParseScannedFilesTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(ds, fs); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -550,8 +550,8 @@ public class ParseScannedFilesTests : AbstractDbTest Assert.Equal(4, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count)); spiceAndWolf.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(2)); - _context.Series.Update(spiceAndWolf); - await _context.SaveChangesAsync(); + Context.Series.Update(spiceAndWolf); + await Context.SaveChangesAsync(); // Add file in subfolder var spiceAndWolfDir = Path.Join(Path.Join(testDirectoryPath, "Spice and Wolf"), "Spice and Wolf Vol. 3"); @@ -559,7 +559,7 @@ public class ParseScannedFilesTests : AbstractDbTest Path.Join(spiceAndWolfDir, "Spice and Wolf Vol. 3 Ch. 0013.cbz")); var res = await psf.ScanFiles(testDirectoryPath, true, - await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); + await UnitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib); var changes = res.Count(sc => sc.HasChanged); Assert.Equal(2, changes); } diff --git a/API.Tests/Services/PersonServiceTests.cs b/API.Tests/Services/PersonServiceTests.cs new file mode 100644 index 000000000..5c1929b1c --- /dev/null +++ b/API.Tests/Services/PersonServiceTests.cs @@ -0,0 +1,286 @@ +using System.Linq; +using System.Threading.Tasks; +using API.Data.Repositories; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Person; +using API.Extensions; +using API.Helpers.Builders; +using API.Services; +using Xunit; + +namespace API.Tests.Services; + +public class PersonServiceTests: AbstractDbTest +{ + + [Fact] + public async Task PersonMerge_KeepNonEmptyMetadata() + { + var ps = new PersonService(UnitOfWork); + + var person1 = new Person + { + Name = "Casey Delores", + NormalizedName = "Casey Delores".ToNormalized(), + HardcoverId = "ANonEmptyId", + MalId = 12, + }; + + var person2 = new Person + { + Name= "Delores Casey", + NormalizedName = "Delores Casey".ToNormalized(), + Description = "Hi, I'm Delores Casey!", + Aliases = [new PersonAliasBuilder("Casey, Delores").Build()], + AniListId = 27, + }; + + UnitOfWork.PersonRepository.Attach(person1); + UnitOfWork.PersonRepository.Attach(person2); + await UnitOfWork.CommitAsync(); + + await ps.MergePeopleAsync(person2, person1); + + var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Single(allPeople); + + var person = allPeople[0]; + Assert.Equal("Casey Delores", person.Name); + Assert.NotEmpty(person.Description); + Assert.Equal(27, person.AniListId); + Assert.NotNull(person.HardcoverId); + Assert.NotEmpty(person.HardcoverId); + Assert.Contains(person.Aliases, pa => pa.Alias == "Delores Casey"); + Assert.Contains(person.Aliases, pa => pa.Alias == "Casey, Delores"); + } + + [Fact] + public async Task PersonMerge_MergedPersonDestruction() + { + var ps = new PersonService(UnitOfWork); + + var person1 = new Person + { + Name = "Casey Delores", + NormalizedName = "Casey Delores".ToNormalized(), + }; + + var person2 = new Person + { + Name = "Delores Casey", + NormalizedName = "Delores Casey".ToNormalized(), + }; + + UnitOfWork.PersonRepository.Attach(person1); + UnitOfWork.PersonRepository.Attach(person2); + await UnitOfWork.CommitAsync(); + + await ps.MergePeopleAsync(person2, person1); + var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Single(allPeople); + } + + [Fact] + public async Task PersonMerge_RetentionChapters() + { + var ps = new PersonService(UnitOfWork); + + var library = new LibraryBuilder("My Library").Build(); + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var user = new AppUserBuilder("Amelia", "amelia@localhost") + .WithLibrary(library).Build(); + UnitOfWork.UserRepository.Add(user); + + var person = new PersonBuilder("Jillian Cowan").Build(); + + var person2 = new PersonBuilder("Cowan Jillian").Build(); + + var chapter = new ChapterBuilder("1") + .WithPerson(person, PersonRole.Editor) + .Build(); + + var chapter2 = new ChapterBuilder("2") + .WithPerson(person2, PersonRole.Editor) + .Build(); + + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .Build(); + + var series2 = new SeriesBuilder("Test 2") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("2") + .WithChapter(chapter2) + .Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + UnitOfWork.SeriesRepository.Add(series2); + await UnitOfWork.CommitAsync(); + + await ps.MergePeopleAsync(person2, person); + + var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Single(allPeople); + var mergedPerson = allPeople[0]; + + Assert.Equal("Jillian Cowan", mergedPerson.Name); + + var chapters = await UnitOfWork.PersonRepository.GetChaptersForPersonByRole(1, 1, PersonRole.Editor); + Assert.Equal(2, chapters.Count()); + + chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(1, ChapterIncludes.People); + Assert.NotNull(chapter); + Assert.Single(chapter.People); + + chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(2, ChapterIncludes.People); + Assert.NotNull(chapter2); + Assert.Single(chapter2.People); + + Assert.Equal(chapter.People.First().PersonId, chapter2.People.First().PersonId); + } + + [Fact] + public async Task PersonMerge_NoDuplicateChaptersOrSeries() + { + await ResetDb(); + + var ps = new PersonService(UnitOfWork); + + var library = new LibraryBuilder("My Library").Build(); + UnitOfWork.LibraryRepository.Add(library); + await UnitOfWork.CommitAsync(); + + var user = new AppUserBuilder("Amelia", "amelia@localhost") + .WithLibrary(library).Build(); + UnitOfWork.UserRepository.Add(user); + + var person = new PersonBuilder("Jillian Cowan").Build(); + + var person2 = new PersonBuilder("Cowan Jillian").Build(); + + var chapter = new ChapterBuilder("1") + .WithPerson(person, PersonRole.Editor) + .WithPerson(person2, PersonRole.Colorist) + .Build(); + + var chapter2 = new ChapterBuilder("2") + .WithPerson(person2, PersonRole.Editor) + .WithPerson(person, PersonRole.Editor) + .Build(); + + var series = new SeriesBuilder("Test 1") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("1") + .WithChapter(chapter) + .Build()) + .WithMetadata(new SeriesMetadataBuilder() + .WithPerson(person, PersonRole.Editor) + .WithPerson(person2, PersonRole.Editor) + .Build()) + .Build(); + + var series2 = new SeriesBuilder("Test 2") + .WithLibraryId(library.Id) + .WithVolume(new VolumeBuilder("2") + .WithChapter(chapter2) + .Build()) + .WithMetadata(new SeriesMetadataBuilder() + .WithPerson(person, PersonRole.Editor) + .WithPerson(person2, PersonRole.Colorist) + .Build()) + .Build(); + + UnitOfWork.SeriesRepository.Add(series); + UnitOfWork.SeriesRepository.Add(series2); + await UnitOfWork.CommitAsync(); + + await ps.MergePeopleAsync(person2, person); + var allPeople = await UnitOfWork.PersonRepository.GetAllPeople(); + Assert.Single(allPeople); + + var mergedPerson = await UnitOfWork.PersonRepository.GetPersonById(person.Id, PersonIncludes.All); + Assert.NotNull(mergedPerson); + Assert.Equal(3, mergedPerson.ChapterPeople.Count); + Assert.Equal(3, mergedPerson.SeriesMetadataPeople.Count); + + chapter = await UnitOfWork.ChapterRepository.GetChapterAsync(chapter.Id, ChapterIncludes.People); + Assert.NotNull(chapter); + Assert.Equal(2, chapter.People.Count); + Assert.Single(chapter.People.Select(p => p.Person.Id).Distinct()); + Assert.Contains(chapter.People, p => p.Role == PersonRole.Editor); + Assert.Contains(chapter.People, p => p.Role == PersonRole.Colorist); + + chapter2 = await UnitOfWork.ChapterRepository.GetChapterAsync(chapter2.Id, ChapterIncludes.People); + Assert.NotNull(chapter2); + Assert.Single(chapter2.People); + Assert.Contains(chapter2.People, p => p.Role == PersonRole.Editor); + Assert.DoesNotContain(chapter2.People, p => p.Role == PersonRole.Colorist); + + series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id, SeriesIncludes.Metadata); + Assert.NotNull(series); + Assert.Single(series.Metadata.People); + Assert.Contains(series.Metadata.People, p => p.Role == PersonRole.Editor); + Assert.DoesNotContain(series.Metadata.People, p => p.Role == PersonRole.Colorist); + + series2 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series2.Id, SeriesIncludes.Metadata); + Assert.NotNull(series2); + Assert.Equal(2, series2.Metadata.People.Count); + Assert.Contains(series2.Metadata.People, p => p.Role == PersonRole.Editor); + Assert.Contains(series2.Metadata.People, p => p.Role == PersonRole.Colorist); + + + } + + [Fact] + public async Task PersonAddAlias_NoOverlap() + { + await ResetDb(); + + UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jillian Cowan").Build()); + UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jilly Cowan").WithAlias("Jolly Cowan").Build()); + await UnitOfWork.CommitAsync(); + + var ps = new PersonService(UnitOfWork); + + var person1 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jillian Cowan"); + var person2 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jilly Cowan"); + Assert.NotNull(person1); + Assert.NotNull(person2); + + // Overlap on Name + var success = await ps.UpdatePersonAliasesAsync(person1, ["Jilly Cowan"]); + Assert.False(success); + + // Overlap on alias + success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan"]); + Assert.False(success); + + // No overlap + success = await ps.UpdatePersonAliasesAsync(person2, ["Jilly Joy Cowan"]); + Assert.True(success); + + // Some overlap + success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]); + Assert.False(success); + + // Some overlap + success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]); + Assert.False(success); + + Assert.Single(person2.Aliases); + } + + protected override async Task ResetDb() + { + Context.Person.RemoveRange(Context.Person.ToList()); + + await Context.SaveChangesAsync(); + } +} diff --git a/API.Tests/Services/RatingServiceTests.cs b/API.Tests/Services/RatingServiceTests.cs index 5cb17f8b5..15f4541d7 100644 --- a/API.Tests/Services/RatingServiceTests.cs +++ b/API.Tests/Services/RatingServiceTests.cs @@ -20,7 +20,7 @@ public class RatingServiceTests: AbstractDbTest public RatingServiceTests() { - _ratingService = new RatingService(_unitOfWork, Substitute.For(), Substitute.For>()); + _ratingService = new RatingService(UnitOfWork, Substitute.For(), Substitute.For>()); } [Fact] @@ -28,7 +28,7 @@ public class RatingServiceTests: AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -39,10 +39,10 @@ public class RatingServiceTests: AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); JobStorage.Current = new InMemoryStorage(); var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto @@ -53,7 +53,7 @@ public class RatingServiceTests: AbstractDbTest Assert.True(result); - var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))! + var ratings = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings))! .Ratings; Assert.NotEmpty(ratings); Assert.Equal(3, ratings.First().Rating); @@ -64,7 +64,7 @@ public class RatingServiceTests: AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -75,9 +75,9 @@ public class RatingServiceTests: AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto { @@ -88,7 +88,7 @@ public class RatingServiceTests: AbstractDbTest Assert.True(result); JobStorage.Current = new InMemoryStorage(); - var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) + var ratings = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) .Ratings; Assert.NotEmpty(ratings); Assert.Equal(3, ratings.First().Rating); @@ -103,7 +103,7 @@ public class RatingServiceTests: AbstractDbTest Assert.True(result2); - var ratings2 = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) + var ratings2 = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)) .Ratings; Assert.NotEmpty(ratings2); Assert.True(ratings2.Count == 1); @@ -115,7 +115,7 @@ public class RatingServiceTests: AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -125,9 +125,9 @@ public class RatingServiceTests: AbstractDbTest .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto { @@ -138,7 +138,7 @@ public class RatingServiceTests: AbstractDbTest Assert.True(result); JobStorage.Current = new InMemoryStorage(); - var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", + var ratings = (await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings)!) .Ratings; Assert.NotEmpty(ratings); @@ -150,7 +150,7 @@ public class RatingServiceTests: AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -160,9 +160,9 @@ public class RatingServiceTests: AbstractDbTest .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Ratings); var result = await _ratingService.UpdateSeriesRating(user, new UpdateRatingDto { @@ -177,13 +177,13 @@ public class RatingServiceTests: AbstractDbTest } protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series.ToList()); - _context.AppUserRating.RemoveRange(_context.AppUserRating.ToList()); - _context.Genre.RemoveRange(_context.Genre.ToList()); - _context.CollectionTag.RemoveRange(_context.CollectionTag.ToList()); - _context.Person.RemoveRange(_context.Person.ToList()); - _context.Library.RemoveRange(_context.Library.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); + Context.AppUserRating.RemoveRange(Context.AppUserRating.ToList()); + Context.Genre.RemoveRange(Context.Genre.ToList()); + Context.CollectionTag.RemoveRange(Context.CollectionTag.ToList()); + Context.Person.RemoveRange(Context.Person.ToList()); + Context.Library.RemoveRange(Context.Library.ToList()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } } diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 102ea3b81..0e4ab2701 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -1,25 +1,19 @@ using System.Collections.Generic; -using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; -using API.Data; using API.Data.Repositories; using API.DTOs.Progress; using API.DTOs.Reader; using API.Entities; using API.Entities.Enums; using API.Extensions; -using API.Helpers; using API.Helpers.Builders; using API.Services; using API.Services.Plus; using API.SignalR; -using AutoMapper; using Hangfire; using Hangfire.InMemory; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -27,25 +21,16 @@ using Xunit.Abstractions; namespace API.Tests.Services; -public class ReaderServiceTests: AbstractFsTest +public class ReaderServiceTests: AbstractDbTest { private readonly ITestOutputHelper _testOutputHelper; - private readonly IUnitOfWork _unitOfWork; - private readonly DataContext _context; private readonly ReaderService _readerService; public ReaderServiceTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options; - _context = new DataContext(contextOptions); - Task.Run(SeedDb).GetAwaiter().GetResult(); - - var config = new MapperConfiguration(cfg => cfg.AddProfile()); - var mapper = config.CreateMapper(); - _unitOfWork = new UnitOfWork(_context, mapper, null); - _readerService = new ReaderService(_unitOfWork, Substitute.For>(), + _readerService = new ReaderService(UnitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); @@ -53,42 +38,12 @@ public class ReaderServiceTests: AbstractFsTest #region Setup - private static DbConnection CreateInMemoryDatabase() + + protected override async Task ResetDb() { - var connection = new SqliteConnection("Filename=:memory:"); + Context.Series.RemoveRange(Context.Series.ToList()); - connection.Open(); - - return connection; - } - - private async Task SeedDb() - { - await _context.Database.MigrateAsync(); - var filesystem = CreateFileSystem(); - - await Seed.SeedSettings(_context, - new DirectoryService(Substitute.For>(), filesystem)); - - 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; - - _context.ServerSetting.Update(setting); - - _context.Library.Add(new LibraryBuilder("Manga") - .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) - .Build()); - return await _context.SaveChangesAsync() > 0; - } - - private async Task ResetDb() - { - _context.Series.RemoveRange(_context.Series.ToList()); - - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } #endregion @@ -122,10 +77,10 @@ public class ReaderServiceTests: AbstractFsTest series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); Assert.Equal(0, (await _readerService.CapPageToChapter(1, -1)).Item1); @@ -150,14 +105,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); JobStorage.Current = new InMemoryStorage(); @@ -171,7 +126,7 @@ public class ReaderServiceTests: AbstractFsTest }, 1); Assert.True(successful); - Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); + Assert.NotNull(await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); } [Fact] @@ -188,14 +143,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); JobStorage.Current = new InMemoryStorage(); var successful = await _readerService.SaveReadingProgress(new ProgressDto() @@ -208,7 +163,7 @@ public class ReaderServiceTests: AbstractFsTest }, 1); Assert.True(successful); - Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); + Assert.NotNull(await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); Assert.True(await _readerService.SaveReadingProgress(new ProgressDto() { @@ -219,7 +174,9 @@ public class ReaderServiceTests: AbstractFsTest BookScrollId = "/h1/" }, 1)); - Assert.Equal("/h1/", (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).BookScrollId); + var userProgress = await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1); + Assert.NotNull(userProgress); + Assert.Equal("/h1/", userProgress.BookScrollId); } @@ -245,22 +202,24 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1); - await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); - await _context.SaveChangesAsync(); + var volumes = await UnitOfWork.VolumeRepository.GetVolumes(1); + await _readerService.MarkChaptersAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); + await Context.SaveChangesAsync(); - Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + var userProgress = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + Assert.NotNull(userProgress); + Assert.Equal(2, userProgress.Progresses.Count); } #endregion @@ -283,27 +242,27 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); - await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); + var volumes = (await UnitOfWork.VolumeRepository.GetVolumes(1)).ToList(); + await _readerService.MarkChaptersAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes[0].Chapters); - await _context.SaveChangesAsync(); - Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + await Context.SaveChangesAsync(); + Assert.Equal(2, (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); - await _readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); - await _context.SaveChangesAsync(); + await _readerService.MarkChaptersAsUnread(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes[0].Chapters); + await Context.SaveChangesAsync(); - var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; + var progresses = (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; Assert.Equal(0, progresses.Max(p => p.PagesRead)); Assert.Equal(2, progresses.Count); } @@ -336,19 +295,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("2", actualChapter.Range); } @@ -370,17 +329,17 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test Lib", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("3-4", actualChapter.Volume.Name); Assert.Equal("1", actualChapter.Range); @@ -413,19 +372,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("31", actualChapter.Range); } @@ -453,18 +412,18 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("21", actualChapter.Range); } @@ -492,19 +451,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("21", actualChapter.Range); } @@ -527,18 +486,18 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("21", actualChapter.Range); } @@ -564,21 +523,21 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal(API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter, actualChapter.Range); } @@ -601,13 +560,13 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 4, 1); Assert.Equal(-1, nextChapter); @@ -626,13 +585,13 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.Equal(-1, nextChapter); @@ -651,13 +610,13 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.Equal(-1, nextChapter); @@ -680,13 +639,13 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.Equal(-1, nextChapter); @@ -716,13 +675,13 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.Equal(-1, nextChapter); @@ -754,19 +713,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("A.cbz", actualChapter.Range); } @@ -792,19 +751,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("A.cbz", actualChapter.Range); } @@ -834,14 +793,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 3, 4, 1); Assert.Equal(-1, nextChapter); @@ -871,19 +830,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.NotEqual(-1, nextChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.NotNull(actualChapter); Assert.Equal("B.cbz", actualChapter.Range); } @@ -904,21 +863,21 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); var user = new AppUserBuilder("majora2007", "fake").Build(); - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); await _readerService.MarkChaptersAsRead(user, 1, new List() { - series.Volumes.First().Chapters.First() + series.Volumes[0].Chapters[0] }); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); Assert.Equal(2, actualChapter.Volume.MinNumber); } @@ -950,19 +909,19 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 2, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("1", actualChapter.Range); } @@ -990,18 +949,18 @@ public class ReaderServiceTests: AbstractFsTest .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 3, 5, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("22", actualChapter.Range); } @@ -1039,20 +998,20 @@ public class ReaderServiceTests: AbstractFsTest .Build()) .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.Series.Add(series); + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // prevChapter should be id from ch.21 from volume 2001 var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 5, 7, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("21", actualChapter.Range); } @@ -1080,20 +1039,20 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("2", actualChapter.Range); } @@ -1116,21 +1075,21 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); Assert.Equal(2, prevChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("2", actualChapter.Range); } @@ -1148,14 +1107,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1176,14 +1135,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1209,14 +1168,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 3, 1); Assert.Equal(-1, prevChapter); @@ -1246,20 +1205,20 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2,5, 1); - var chapterInfoDto = await _unitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter); + var chapterInfoDto = await UnitOfWork.ChapterRepository.GetChapterInfoDtoAsync(prevChapter); Assert.Equal(1, chapterInfoDto.ChapterNumber.AsFloat()); // This is first chapter of first volume @@ -1280,14 +1239,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1319,22 +1278,22 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 4, 1); Assert.NotEqual(-1, prevChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("A.cbz", actualChapter.Range); } @@ -1357,18 +1316,18 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var prevChapter = await _readerService.GetPrevChapterIdAsync(1, 1, 1, 1); Assert.NotEqual(-1, prevChapter); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.NotNull(actualChapter); Assert.Equal("22", actualChapter.Range); } @@ -1389,16 +1348,16 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); var user = new AppUserBuilder("majora2007", "fake").Build(); - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1); - var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); + var actualChapter = await UnitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); Assert.Equal(1, actualChapter.Volume.MinNumber); } @@ -1431,15 +1390,15 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1464,14 +1423,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1503,14 +1462,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1548,14 +1507,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1583,7 +1542,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 2 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1627,14 +1586,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1656,7 +1615,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 3 // Volume 2 id }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1682,14 +1641,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1715,14 +1674,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1750,7 +1709,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 2 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1777,15 +1736,15 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1815,22 +1774,22 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); var user = new AppUser() { UserName = "majora2007" }; - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Mark everything but chapter 101 as read await _readerService.MarkSeriesAsRead(user, 1); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); // Unmark last chapter as read - var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1); + var vol = await UnitOfWork.VolumeRepository.GetVolumeByIdAsync(1); foreach (var chapt in vol.Chapters) { await _readerService.SaveReadingProgress(new ProgressDto() @@ -1841,7 +1800,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 1 }, 1); } - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1869,22 +1828,22 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); var user = new AppUser() { UserName = "majora2007" }; - _context.AppUser.Add(user); + Context.AppUser.Add(user); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Mark everything but chapter 101 as read await _readerService.MarkSeriesAsRead(user, 1); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); // Unmark last chapter as read - var vol = await _unitOfWork.VolumeRepository.GetVolumeByIdAsync(1); + var vol = await UnitOfWork.VolumeRepository.GetVolumeByIdAsync(1); await _readerService.SaveReadingProgress(new ProgressDto() { PageNum = 0, @@ -1899,7 +1858,7 @@ public class ReaderServiceTests: AbstractFsTest SeriesId = 1, VolumeId = 1 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1922,14 +1881,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -1956,7 +1915,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 2 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -1982,21 +1941,21 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Save progress on first volume chapters and 1st of second volume - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await _readerService.MarkSeriesAsRead(user, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2020,14 +1979,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); @@ -2054,7 +2013,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 1 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2083,20 +2042,20 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await _readerService.MarkSeriesAsRead(user, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Add 2 new unread series to the Series series.Volumes[0].Chapters.Add(new ChapterBuilder("231") @@ -2105,8 +2064,8 @@ public class ReaderServiceTests: AbstractFsTest series.Volumes[2].Chapters.Add(new ChapterBuilder("14.9") .WithPages(1) .Build()); - _context.Series.Attach(series); - await _context.SaveChangesAsync(); + Context.Series.Attach(series); + await Context.SaveChangesAsync(); // This tests that if you add a series later to a volume and a loose leaf chapter, we continue from that volume, rather than loose leaf var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2146,26 +2105,26 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Save progress on first volume chapters and 1st of second volume - var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress); await _readerService.MarkChaptersAsRead(user, 1, new List() { readChapter1, readChapter2 }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2202,14 +2161,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); await _readerService.SaveReadingProgress(new ProgressDto() { @@ -2267,7 +2226,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 2 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2296,14 +2255,14 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); await _readerService.SaveReadingProgress(new ProgressDto() { @@ -2313,7 +2272,7 @@ public class ReaderServiceTests: AbstractFsTest VolumeId = 1 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2328,7 +2287,7 @@ public class ReaderServiceTests: AbstractFsTest SeriesId = 1, VolumeId = 1 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2343,7 +2302,7 @@ public class ReaderServiceTests: AbstractFsTest SeriesId = 1, VolumeId = 1 }, 1); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); nextChapter = await _readerService.GetContinuePoint(1, 1); @@ -2373,26 +2332,26 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 5); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); } [Fact] @@ -2413,27 +2372,27 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkChaptersUntilAsRead(user, 1, 2.5f); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1)).PagesRead); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(4, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))); } [Fact] @@ -2451,23 +2410,24 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + Assert.NotNull(user); await _readerService.MarkChaptersUntilAsRead(user, 1, 2); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Validate correct chapters have read status - Assert.True(await _unitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); + Assert.True(await UnitOfWork.AppUserProgressRepository.UserHasProgress(LibraryType.Manga, 1)); } [Fact] @@ -2502,24 +2462,24 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); const int markReadUntilNumber = 47; await _readerService.MarkChaptersUntilAsRead(user, 1, markReadUntilNumber); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var volumes = await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(1, 1); + var volumes = await UnitOfWork.VolumeRepository.GetVolumesDtoAsync(1, 1); Assert.True(volumes.SelectMany(v => v.Chapters).All(c => { // Specials are ignored. @@ -2556,21 +2516,21 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - await _readerService.MarkSeriesAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); - await _context.SaveChangesAsync(); + await _readerService.MarkSeriesAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); + await Context.SaveChangesAsync(); - Assert.Equal(4, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + Assert.Equal(4, (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); } @@ -2591,27 +2551,27 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); - await _readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); + var volumes = (await UnitOfWork.VolumeRepository.GetVolumes(1)).ToList(); + await _readerService.MarkChaptersAsRead(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes[0].Chapters); - await _context.SaveChangesAsync(); - Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); + await Context.SaveChangesAsync(); + Assert.Equal(2, (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); - await _readerService.MarkSeriesAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); - await _context.SaveChangesAsync(); + await _readerService.MarkSeriesAsUnread(await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1); + await Context.SaveChangesAsync(); - var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; + var progresses = (await UnitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; Assert.Equal(0, progresses.Max(p => p.PagesRead)); Assert.Equal(2, progresses.Count); } @@ -2679,31 +2639,32 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); await _readerService.MarkVolumesUntilAsRead(user, 1, 2002); - await _context.SaveChangesAsync(); + Assert.NotNull(user); + await Context.SaveChangesAsync(); // Validate loose leaf chapters don't get marked as read - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); // Validate that volumes 1997 and 2002 both have their respective chapter 0 marked as read - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead); - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead); // Validate that the chapter 0 of the following volume (2003) is not read - Assert.Null(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(7, 1)); + Assert.Null(await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(7, 1)); } @@ -2734,30 +2695,31 @@ public class ReaderServiceTests: AbstractFsTest .Build(); series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); - _context.Series.Add(series); + Context.Series.Add(series); - _context.AppUser.Add(new AppUser() + Context.AppUser.Add(new AppUser() { UserName = "majora2007" }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + var user = await UnitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", AppUserIncludes.Progress); + Assert.NotNull(user); await _readerService.MarkVolumesUntilAsRead(user, 1, 2002); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Validate loose leaf chapters don't get marked as read - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(2, 1))); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); // Validate volumes chapter 0 have read status - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1)).PagesRead); - Assert.Equal(1, (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1)).PagesRead); - Assert.Null((await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(5, 1))?.PagesRead); + Assert.Equal(1, (await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(6, 1))?.PagesRead); + Assert.Null((await UnitOfWork.AppUserProgressRepository.GetUserProgressAsync(3, 1))); } #endregion diff --git a/API.Tests/Services/ScannerServiceTests.cs b/API.Tests/Services/ScannerServiceTests.cs index 4554820fb..2e812647b 100644 --- a/API.Tests/Services/ScannerServiceTests.cs +++ b/API.Tests/Services/ScannerServiceTests.cs @@ -28,13 +28,13 @@ public class ScannerServiceTests : AbstractDbTest // Set up Hangfire to use in-memory storage for testing GlobalConfiguration.Configuration.UseInMemoryStorage(); - _scannerHelper = new ScannerHelper(_unitOfWork, testOutputHelper); + _scannerHelper = new ScannerHelper(UnitOfWork, testOutputHelper); } protected override async Task ResetDb() { - _context.Library.RemoveRange(_context.Library); - await _context.SaveChangesAsync(); + Context.Library.RemoveRange(Context.Library); + await Context.SaveChangesAsync(); } @@ -44,18 +44,18 @@ public class ScannerServiceTests : AbstractDbTest { await SetLastScannedInThePast(series, duration, false); } - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } protected async Task SetLastScannedInThePast(Series series, TimeSpan? duration = null, bool save = true) { duration ??= TimeSpan.FromMinutes(2); series.LastFolderScanned = DateTime.Now.Subtract(duration.Value); - _context.Series.Update(series); + Context.Series.Update(series); if (save) { - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } } @@ -66,7 +66,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(4, postLib.Series.Count); @@ -79,7 +79,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -94,7 +94,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -110,7 +110,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -125,7 +125,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -141,7 +141,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -157,7 +157,7 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase); var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -188,7 +188,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -213,7 +213,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -244,7 +244,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -268,7 +268,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -288,7 +288,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -326,7 +326,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -350,7 +350,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -369,7 +369,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -399,7 +399,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -436,7 +436,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -464,7 +464,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -484,13 +484,13 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**/Extra/*"}]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -508,13 +508,13 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**\\Extra\\*"}]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -541,13 +541,13 @@ public class ScannerServiceTests : AbstractDbTest new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 2")} ]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(2, postLib.Series.Count); @@ -563,7 +563,7 @@ public class ScannerServiceTests : AbstractDbTest // Rescan to ensure nothing changes yet again await scanner.ScanLibrary(library.Id, true); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.Equal(2, postLib.Series.Count); s = postLib.Series.First(s => s.Name == "Plush"); Assert.Equal(3, s.Volumes.Count); @@ -594,13 +594,13 @@ public class ScannerServiceTests : AbstractDbTest new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 2")} ]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(2, postLib.Series.Count); @@ -619,7 +619,7 @@ public class ScannerServiceTests : AbstractDbTest // Rescan to ensure nothing changes yet again await scanner.ScanLibrary(library.Id, false); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.Equal(2, postLib.Series.Count); s = postLib.Series.First(s => s.Name == "Plush"); Assert.Equal(3, s.Volumes.Count); @@ -647,14 +647,14 @@ public class ScannerServiceTests : AbstractDbTest new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } ]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var scanner = _scannerHelper.CreateServices(); // First Scan: Everything should be added await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Contains(postLib.Series, s => s.Name == "Accel"); @@ -662,19 +662,19 @@ public class ScannerServiceTests : AbstractDbTest // Second Scan: Remove Root 2, expect Accel to be removed library.Folders = [new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); // Emulate time passage by updating lastFolderScan to be a min in the past foreach (var s in postLib.Series) { s.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(1)); - _context.Series.Update(s); + Context.Series.Update(s); } - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); await scanner.ScanLibrary(library.Id); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.DoesNotContain(postLib.Series, s => s.Name == "Accel"); // Ensure Accel is gone Assert.Contains(postLib.Series, s => s.Name == "Plush"); @@ -685,19 +685,19 @@ public class ScannerServiceTests : AbstractDbTest new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 1") }, new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } ]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); // Emulate time passage by updating lastFolderScan to be a min in the past foreach (var s in postLib.Series) { s.LastFolderScanned = DateTime.Now.Subtract(TimeSpan.FromMinutes(1)); - _context.Series.Update(s); + Context.Series.Update(s); } - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); await scanner.ScanLibrary(library.Id); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.Contains(postLib.Series, s => s.Name == "Accel"); // Accel should be back Assert.Contains(postLib.Series, s => s.Name == "Plush"); @@ -707,7 +707,7 @@ public class ScannerServiceTests : AbstractDbTest // Fourth Scan: Run again to check stability (should not remove Accel) await scanner.ScanLibrary(library.Id); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.Contains(postLib.Series, s => s.Name == "Accel"); Assert.Contains(postLib.Series, s => s.Name == "Plush"); @@ -732,14 +732,14 @@ public class ScannerServiceTests : AbstractDbTest new FolderPath() { Path = Path.Combine(testDirectoryPath, "Root 2") } ]; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + UnitOfWork.LibraryRepository.Update(library); + await UnitOfWork.CommitAsync(); var scanner = _scannerHelper.CreateServices(); // First Scan: Everything should be added await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Contains(postLib.Series, s => s.Name == "Accel"); @@ -747,14 +747,14 @@ public class ScannerServiceTests : AbstractDbTest // Second Scan: Delete the Series library.Series = []; - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Empty(postLib.Series); await scanner.ScanLibrary(library.Id); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.Contains(postLib.Series, s => s.Name == "Accel"); // Ensure Accel is gone Assert.Contains(postLib.Series, s => s.Name == "Plush"); @@ -768,13 +768,13 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(4, postLib.Series.Count); @@ -800,9 +800,9 @@ public class ScannerServiceTests : AbstractDbTest Path.Join(executionerCopyDir, "The Executioner and Her Way of Life Vol. 1 Ch. 0002.cbz")); await scanner.ScanLibrary(library.Id); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(4, postLib.Series.Count); @@ -827,13 +827,13 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Equal(2, postLib.Series.Count); @@ -841,9 +841,9 @@ public class ScannerServiceTests : AbstractDbTest Directory.Delete(executionerCopyDir, true); await scanner.ScanLibrary(library.Id); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); Assert.Single(postLib.Series, s => s.Name == "Spice and Wolf"); @@ -859,13 +859,13 @@ public class ScannerServiceTests : AbstractDbTest var library = await _scannerHelper.GenerateScannerData(testcase, infos); var testDirectoryPath = library.Folders.First().Path; - _unitOfWork.LibraryRepository.Update(library); - await _unitOfWork.CommitAsync(); + 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); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -882,7 +882,7 @@ public class ScannerServiceTests : AbstractDbTest await scanner.ScanLibrary(library.Id); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -899,7 +899,7 @@ public class ScannerServiceTests : AbstractDbTest await scanner.ScanLibrary(library.Id); - postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); Assert.Single(postLib.Series); @@ -923,7 +923,7 @@ public class ScannerServiceTests : AbstractDbTest var scanner = _scannerHelper.CreateServices(); await scanner.ScanLibrary(library.Id); - var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); + var postLib = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series); Assert.NotNull(postLib); // Get the loose leaf volume and confirm each chapter aligns with expectation of Sort Order diff --git a/API.Tests/Services/ScrobblingServiceTests.cs b/API.Tests/Services/ScrobblingServiceTests.cs index b7a418d83..50398a146 100644 --- a/API.Tests/Services/ScrobblingServiceTests.cs +++ b/API.Tests/Services/ScrobblingServiceTests.cs @@ -28,17 +28,17 @@ public class ScrobblingServiceTests : AbstractDbTest _logger = Substitute.For>(); _emailService = Substitute.For(); - _service = new ScrobblingService(_unitOfWork, Substitute.For(), _logger, _licenseService, _localizationService, _emailService); + _service = new ScrobblingService(UnitOfWork, Substitute.For(), _logger, _licenseService, _localizationService, _emailService); } protected override async Task ResetDb() { - _context.ScrobbleEvent.RemoveRange(_context.ScrobbleEvent.ToList()); - _context.Series.RemoveRange(_context.Series.ToList()); - _context.Library.RemoveRange(_context.Library.ToList()); - _context.AppUser.RemoveRange(_context.AppUser.ToList()); + Context.ScrobbleEvent.RemoveRange(Context.ScrobbleEvent.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); + Context.Library.RemoveRange(Context.Library.ToList()); + Context.AppUser.RemoveRange(Context.AppUser.ToList()); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); } private async Task SeedData() @@ -54,7 +54,7 @@ public class ScrobblingServiceTests : AbstractDbTest .Build(); - _context.Library.Add(library); + Context.Library.Add(library); var user = new AppUserBuilder("testuser", "testuser") //.WithPreferences(new UserPreferencesBuilder().WithAniListScrobblingEnabled(true).Build()) @@ -62,9 +62,9 @@ public class ScrobblingServiceTests : AbstractDbTest user.UserPreferences.AniListScrobblingEnabled = true; - _unitOfWork.UserRepository.Add(user); + UnitOfWork.UserRepository.Add(user); - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); } #region ScrobbleWantToReadUpdate Tests @@ -83,7 +83,7 @@ public class ScrobblingServiceTests : AbstractDbTest await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); // Assert - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); Assert.Single(events); Assert.Equal(ScrobbleEventType.AddWantToRead, events[0].ScrobbleEventType); Assert.Equal(userId, events[0].AppUserId); @@ -103,7 +103,7 @@ public class ScrobblingServiceTests : AbstractDbTest await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); // Assert - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); Assert.Single(events); Assert.Equal(ScrobbleEventType.RemoveWantToRead, events[0].ScrobbleEventType); Assert.Equal(userId, events[0].AppUserId); @@ -126,7 +126,7 @@ public class ScrobblingServiceTests : AbstractDbTest await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); // Assert - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); Assert.Single(events); Assert.All(events, e => Assert.Equal(ScrobbleEventType.AddWantToRead, e.ScrobbleEventType)); @@ -149,7 +149,7 @@ public class ScrobblingServiceTests : AbstractDbTest await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); // Assert - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); Assert.Single(events); Assert.Contains(events, e => e.ScrobbleEventType == ScrobbleEventType.RemoveWantToRead); @@ -172,7 +172,7 @@ public class ScrobblingServiceTests : AbstractDbTest await _service.ScrobbleWantToReadUpdate(userId, seriesId, false); // Assert - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); Assert.Single(events); Assert.All(events, e => Assert.Equal(ScrobbleEventType.RemoveWantToRead, e.ScrobbleEventType)); @@ -195,7 +195,7 @@ public class ScrobblingServiceTests : AbstractDbTest await _service.ScrobbleWantToReadUpdate(userId, seriesId, true); // Assert - var events = await _unitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); + var events = await UnitOfWork.ScrobbleRepository.GetAllEventsForSeries(seriesId); Assert.Single(events); Assert.Contains(events, e => e.ScrobbleEventType == ScrobbleEventType.AddWantToRead); diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 4bc6d91b4..55babf815 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -8,6 +8,7 @@ using API.Data; using API.Data.Repositories; using API.DTOs; using API.DTOs.Metadata; +using API.DTOs.Person; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; @@ -56,7 +57,7 @@ public class SeriesServiceTests : AbstractDbTest var locService = new LocalizationService(ds, new MockHostingEnvironment(), Substitute.For(), Substitute.For()); - _seriesService = new SeriesService(_unitOfWork, Substitute.For(), + _seriesService = new SeriesService(UnitOfWork, Substitute.For(), Substitute.For(), Substitute.For>(), Substitute.For(), locService, Substitute.For()); } @@ -65,14 +66,14 @@ public class SeriesServiceTests : AbstractDbTest protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series.ToList()); - _context.AppUserRating.RemoveRange(_context.AppUserRating.ToList()); - _context.Genre.RemoveRange(_context.Genre.ToList()); - _context.CollectionTag.RemoveRange(_context.CollectionTag.ToList()); - _context.Person.RemoveRange(_context.Person.ToList()); - _context.Library.RemoveRange(_context.Library.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); + Context.AppUserRating.RemoveRange(Context.AppUserRating.ToList()); + Context.Genre.RemoveRange(Context.Genre.ToList()); + Context.CollectionTag.RemoveRange(Context.CollectionTag.ToList()); + Context.Person.RemoveRange(Context.Person.ToList()); + Context.Library.RemoveRange(Context.Library.ToList()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } private static UpdateRelatedSeriesDto CreateRelationsDto(Series series) @@ -105,7 +106,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -126,7 +127,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var expectedRanges = new[] {"Omake", "Something SP02"}; @@ -141,7 +142,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -162,7 +163,7 @@ public class SeriesServiceTests : AbstractDbTest .Build() ); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Chapters); @@ -178,7 +179,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -196,7 +197,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Chapters); @@ -212,7 +213,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") .WithVolume(new VolumeBuilder(Parser.LooseLeafVolume) @@ -229,7 +230,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Chapters); @@ -248,7 +249,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -263,7 +264,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Volumes); @@ -277,7 +278,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -294,7 +295,7 @@ public class SeriesServiceTests : AbstractDbTest - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.NotEmpty(detail.Volumes); @@ -314,7 +315,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -332,7 +333,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); Assert.Equal("Volume 1", detail.Volumes.ElementAt(0).Name); @@ -349,7 +350,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -373,7 +374,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); @@ -400,7 +401,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Comic) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Comic) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -424,7 +425,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); @@ -450,7 +451,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.ComicVine) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.ComicVine) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -474,7 +475,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); @@ -500,7 +501,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -522,7 +523,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); @@ -548,7 +549,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.LightNovel) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.LightNovel) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -570,7 +571,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()) .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var detail = await _seriesService.GetSeriesDetail(1, 1); @@ -602,8 +603,8 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - _context.Series.Add(s); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -617,7 +618,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); @@ -634,10 +635,10 @@ public class SeriesServiceTests : AbstractDbTest var g = new GenreBuilder("Existing Genre").Build(); s.Metadata.Genres = new List {g}; - _context.Series.Add(s); + Context.Series.Add(s); - _context.Genre.Add(g); - await _context.SaveChangesAsync(); + Context.Genre.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -651,7 +652,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase())); @@ -663,7 +664,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); var g = new PersonBuilder("Existing Person").Build(); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var s = new SeriesBuilder("Test") .WithMetadata(new SeriesMetadataBuilder() @@ -673,10 +674,10 @@ public class SeriesServiceTests : AbstractDbTest s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - _context.Series.Add(s); + Context.Series.Add(s); - _context.Person.Add(g); - await _context.SaveChangesAsync(); + Context.Person.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -690,7 +691,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); @@ -713,10 +714,10 @@ public class SeriesServiceTests : AbstractDbTest new SeriesMetadataPeople() {Person = new PersonBuilder("Existing Publisher 2").Build(), Role = PersonRole.Publisher} }; - _context.Series.Add(s); + Context.Series.Add(s); - _context.Person.Add(g); - await _context.SaveChangesAsync(); + Context.Person.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -731,7 +732,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person")); @@ -763,9 +764,9 @@ public class SeriesServiceTests : AbstractDbTest new SeriesMetadataPeople { Person = new PersonBuilder("Existing Publisher 2").Build(), Role = PersonRole.Publisher } }; - _context.Series.Add(series); - _context.Person.Add(existingPerson); - await _context.SaveChangesAsync(); + Context.Series.Add(series); + Context.Person.Add(existingPerson); + await Context.SaveChangesAsync(); // Act: Update series metadata, attempting to update the writer to "Existing Writer" var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto @@ -782,7 +783,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); // Reload the series from the database - var updatedSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id); + var updatedSeries = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(series.Id); Assert.NotNull(updatedSeries.Metadata); // Assert that the people list still contains the updated person with the new name @@ -804,10 +805,10 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var g = new PersonBuilder("Existing Person").Build(); - _context.Series.Add(s); + Context.Series.Add(s); - _context.Person.Add(g); - await _context.SaveChangesAsync(); + Context.Person.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -821,7 +822,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.False(series.Metadata.People.Any()); @@ -839,10 +840,10 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var g = new PersonBuilder("Existing Person").Build(); - _context.Series.Add(s); + Context.Series.Add(s); - _context.Person.Add(g); - await _context.SaveChangesAsync(); + Context.Person.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -857,7 +858,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.People.Count != 0); @@ -891,10 +892,10 @@ public class SeriesServiceTests : AbstractDbTest var g = new GenreBuilder("Existing Genre").Build(); s.Metadata.Genres = new List {g}; s.Metadata.GenresLocked = true; - _context.Series.Add(s); + Context.Series.Add(s); - _context.Genre.Add(g); - await _context.SaveChangesAsync(); + Context.Genre.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -909,7 +910,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase())); @@ -924,8 +925,8 @@ public class SeriesServiceTests : AbstractDbTest .WithMetadata(new SeriesMetadataBuilder().Build()) .Build(); s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); - _context.Series.Add(s); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -939,7 +940,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Equal(0, series.Metadata.ReleaseYear); @@ -958,8 +959,8 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - _context.Series.Add(s); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -972,7 +973,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); @@ -991,9 +992,9 @@ public class SeriesServiceTests : AbstractDbTest var g = new GenreBuilder("Existing Genre").Build(); s.Metadata.Genres = new List { g }; - _context.Series.Add(s); - _context.Genre.Add(g); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + Context.Genre.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -1006,7 +1007,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.DoesNotContain("Existing Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); @@ -1025,9 +1026,9 @@ public class SeriesServiceTests : AbstractDbTest var g = new GenreBuilder("Existing Genre").Build(); s.Metadata.Genres = new List { g }; - _context.Series.Add(s); - _context.Genre.Add(g); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + Context.Genre.Add(g); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -1040,7 +1041,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Empty(series.Metadata.Genres); @@ -1058,8 +1059,8 @@ public class SeriesServiceTests : AbstractDbTest .Build(); s.Library = new LibraryBuilder("Test Lib", LibraryType.Book).Build(); - _context.Series.Add(s); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -1072,7 +1073,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); @@ -1090,9 +1091,9 @@ public class SeriesServiceTests : AbstractDbTest var t = new TagBuilder("Existing Tag").Build(); s.Metadata.Tags = new List { t }; - _context.Series.Add(s); - _context.Tag.Add(t); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + Context.Tag.Add(t); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -1105,7 +1106,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.DoesNotContain("Existing Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title)); @@ -1124,9 +1125,9 @@ public class SeriesServiceTests : AbstractDbTest var t = new TagBuilder("Existing Tag").Build(); s.Metadata.Tags = new List { t }; - _context.Series.Add(s); - _context.Tag.Add(t); - await _context.SaveChangesAsync(); + Context.Series.Add(s); + Context.Tag.Add(t); + await Context.SaveChangesAsync(); var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto { @@ -1139,7 +1140,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); - var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); + var series = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id); Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Empty(series.Metadata.Tags); @@ -1276,7 +1277,7 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateRelatedSeries_ShouldAddAllRelations() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1295,9 +1296,9 @@ public class SeriesServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); @@ -1312,7 +1313,7 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateRelatedSeries_ShouldAddPrequelWhenAddingSequel() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1330,10 +1331,10 @@ public class SeriesServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); - var series2 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series2 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Sequels.Add(2); @@ -1348,7 +1349,7 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateRelatedSeries_DeleteAllRelations() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1367,9 +1368,9 @@ public class SeriesServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); @@ -1392,7 +1393,7 @@ public class SeriesServiceTests : AbstractDbTest public async Task UpdateRelatedSeries_DeleteTargetSeries_ShouldSucceed() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1410,9 +1411,9 @@ public class SeriesServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); @@ -1421,10 +1422,10 @@ public class SeriesServiceTests : AbstractDbTest Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); - _context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); + Context.Series.Remove(await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); try { - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } catch (Exception) { @@ -1432,14 +1433,14 @@ public class SeriesServiceTests : AbstractDbTest } // Remove relations - Assert.Empty((await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related)).Relations); + Assert.Empty((await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related)).Relations); } [Fact] public async Task UpdateRelatedSeries_DeleteSourceSeries_ShouldSucceed() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1457,9 +1458,9 @@ public class SeriesServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); @@ -1467,12 +1468,12 @@ public class SeriesServiceTests : AbstractDbTest Assert.NotNull(series1); Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId); - var seriesToRemove = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + var seriesToRemove = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1); Assert.NotNull(seriesToRemove); - _context.Series.Remove(seriesToRemove); + Context.Series.Remove(seriesToRemove); try { - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } catch (Exception) { @@ -1480,14 +1481,14 @@ public class SeriesServiceTests : AbstractDbTest } // Remove relations - Assert.Empty((await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related)).Relations); + Assert.Empty((await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related)).Relations); } [Fact] public async Task UpdateRelatedSeries_ShouldNotAllowDuplicates() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1505,9 +1506,9 @@ public class SeriesServiceTests : AbstractDbTest } }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); var relation = new SeriesRelation { Series = series1, @@ -1532,7 +1533,7 @@ public class SeriesServiceTests : AbstractDbTest public async Task GetRelatedSeries_EditionPrequelSequel_ShouldNotHaveParent() { await ResetDb(); - _context.Library.Add(new Library + Context.Library.Add(new Library { AppUsers = new List { @@ -1552,8 +1553,8 @@ public class SeriesServiceTests : AbstractDbTest new SeriesBuilder("Test Series Adaption").Build(), } }); - await _context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + await Context.SaveChangesAsync(); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Editions.Add(2); @@ -1579,30 +1580,30 @@ public class SeriesServiceTests : AbstractDbTest .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - _context.Library.Add(lib); + Context.Library.Add(lib); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(2); addRelationDto.Sequels.Add(3); await _seriesService.UpdateRelatedSeries(addRelationDto); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib.Id); - _unitOfWork.LibraryRepository.Delete(library); + var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(lib.Id); + UnitOfWork.LibraryRepository.Delete(library); try { - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); } catch (Exception) { Assert.False(true); } - Assert.Null(await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); + Assert.Null(await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); } [Fact] @@ -1623,7 +1624,7 @@ public class SeriesServiceTests : AbstractDbTest .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - _context.Library.Add(lib1); + Context.Library.Add(lib1); var lib2 = new LibraryBuilder("Test LIb 2", LibraryType.Book) .WithSeries(new SeriesBuilder("Test Series 2").Build()) @@ -1631,29 +1632,29 @@ public class SeriesServiceTests : AbstractDbTest .WithSeries(new SeriesBuilder("Test Series Prequels 3").Build())// TODO: Is this a bug .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - _context.Library.Add(lib2); + Context.Library.Add(lib2); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related); // Add relations var addRelationDto = CreateRelationsDto(series1); addRelationDto.Adaptations.Add(4); // cross library link await _seriesService.UpdateRelatedSeries(addRelationDto); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(lib1.Id, LibraryIncludes.Series); - _unitOfWork.LibraryRepository.Delete(library); + var library = await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(lib1.Id, LibraryIncludes.Series); + UnitOfWork.LibraryRepository.Delete(library); try { - await _unitOfWork.CommitAsync(); + await UnitOfWork.CommitAsync(); } catch (Exception) { Assert.False(true); } - Assert.Null(await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); + Assert.Null(await UnitOfWork.LibraryRepository.GetLibraryForIdAsync(1)); } #endregion @@ -1675,13 +1676,13 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty) .WithLocale("en") .Build()) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); Assert.Equal(expected, await _seriesService.FormatChapterName(1, libraryType, withHash)); } @@ -1846,18 +1847,18 @@ public class SeriesServiceTests : AbstractDbTest .WithSeries(new SeriesBuilder("Test Series Sequels").Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - _context.Library.Add(lib1); + Context.Library.Add(lib1); var lib2 = new LibraryBuilder("Test LIb 2", LibraryType.Book) .WithSeries(new SeriesBuilder("Test Series 2").Build()) .WithSeries(new SeriesBuilder("Test Series Prequels 2").Build()) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .Build(); - _context.Library.Add(lib2); + Context.Library.Add(lib2); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, + var series1 = await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related | SeriesIncludes.ExternalRatings); // Add relations var addRelationDto = CreateRelationsDto(series1); @@ -1903,12 +1904,12 @@ public class SeriesServiceTests : AbstractDbTest } }; - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); // Ensure we can delete the series Assert.True(await _seriesService.DeleteMultipleSeries(new[] {1, 2})); - Assert.Null(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1)); - Assert.Null(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); + Assert.Null(await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(1)); + Assert.Null(await UnitOfWork.SeriesRepository.GetSeriesByIdAsync(2)); } #endregion @@ -1920,7 +1921,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -1933,7 +1934,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); Assert.Equal(Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber); @@ -1945,7 +1946,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") .WithPublicationStatus(PublicationStatus.Completed) @@ -1958,7 +1959,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); Assert.Equal(Parser.LooseLeafVolumeNumber, nextChapter.VolumeNumber); @@ -1970,7 +1971,7 @@ public class SeriesServiceTests : AbstractDbTest { await ResetDb(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") @@ -1982,7 +1983,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); Assert.NotNull(nextChapter); @@ -1996,7 +1997,7 @@ public class SeriesServiceTests : AbstractDbTest await ResetDb(); var now = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture); // 10/31/2024 can trigger an edge case bug - _context.Library.Add(new LibraryBuilder("Test LIb") + Context.Library.Add(new LibraryBuilder("Test LIb") .WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build()) .WithSeries(new SeriesBuilder("Test") .WithPublicationStatus(PublicationStatus.OnGoing) @@ -2010,7 +2011,7 @@ public class SeriesServiceTests : AbstractDbTest .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var nextChapter = await _seriesService.GetEstimatedChapterCreationDate(1, 1); Assert.NotNull(nextChapter); diff --git a/API.Tests/Services/SiteThemeServiceTests.cs b/API.Tests/Services/SiteThemeServiceTests.cs index 463d49df4..3893af1fb 100644 --- a/API.Tests/Services/SiteThemeServiceTests.cs +++ b/API.Tests/Services/SiteThemeServiceTests.cs @@ -31,24 +31,24 @@ public abstract class SiteThemeServiceTest : AbstractDbTest protected override async Task ResetDb() { - _context.SiteTheme.RemoveRange(_context.SiteTheme); - await _context.SaveChangesAsync(); + Context.SiteTheme.RemoveRange(Context.SiteTheme); + await Context.SaveChangesAsync(); // Recreate defaults - await Seed.SeedThemes(_context); + await Seed.SeedThemes(Context); } [Fact] public async Task UpdateDefault_ShouldThrowOnInvalidId() { await ResetDb(); - _testOutputHelper.WriteLine($"[UpdateDefault_ShouldThrowOnInvalidId] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + _testOutputHelper.WriteLine($"[UpdateDefault_ShouldThrowOnInvalidId] All Themes: {(await UnitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub, Substitute.For(), + var siteThemeService = new ThemeService(ds, UnitOfWork, _messageHub, Substitute.For(), Substitute.For>(), Substitute.For()); - _context.SiteTheme.Add(new SiteTheme() + Context.SiteTheme.Add(new SiteTheme() { Name = "Custom", NormalizedName = "Custom".ToNormalized(), @@ -56,7 +56,7 @@ public abstract class SiteThemeServiceTest : AbstractDbTest FileName = "custom.css", IsDefault = false }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => siteThemeService.UpdateDefault(10)); Assert.Equal("Theme file missing or invalid", ex.Message); @@ -68,14 +68,14 @@ public abstract class SiteThemeServiceTest : AbstractDbTest public async Task GetContent_ShouldReturnContent() { await ResetDb(); - _testOutputHelper.WriteLine($"[GetContent_ShouldReturnContent] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + _testOutputHelper.WriteLine($"[GetContent_ShouldReturnContent] All Themes: {(await UnitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub, Substitute.For(), + var siteThemeService = new ThemeService(ds, UnitOfWork, _messageHub, Substitute.For(), Substitute.For>(), Substitute.For()); - _context.SiteTheme.Add(new SiteTheme() + Context.SiteTheme.Add(new SiteTheme() { Name = "Custom", NormalizedName = "Custom".ToNormalized(), @@ -83,9 +83,9 @@ public abstract class SiteThemeServiceTest : AbstractDbTest FileName = "custom.css", IsDefault = false }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var content = await siteThemeService.GetContent((await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")).Id); + var content = await siteThemeService.GetContent((await UnitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")).Id); Assert.NotNull(content); Assert.NotEmpty(content); Assert.Equal("123", content); @@ -95,14 +95,14 @@ public abstract class SiteThemeServiceTest : AbstractDbTest public async Task UpdateDefault_ShouldHaveOneDefault() { await ResetDb(); - _testOutputHelper.WriteLine($"[UpdateDefault_ShouldHaveOneDefault] All Themes: {(await _unitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); + _testOutputHelper.WriteLine($"[UpdateDefault_ShouldHaveOneDefault] All Themes: {(await UnitOfWork.SiteThemeRepository.GetThemes()).Count(t => t.IsDefault)}"); var filesystem = CreateFileSystem(); filesystem.AddFile($"{SiteThemeDirectory}custom.css", new MockFileData("123")); var ds = new DirectoryService(Substitute.For>(), filesystem); - var siteThemeService = new ThemeService(ds, _unitOfWork, _messageHub, Substitute.For(), + var siteThemeService = new ThemeService(ds, UnitOfWork, _messageHub, Substitute.For(), Substitute.For>(), Substitute.For()); - _context.SiteTheme.Add(new SiteTheme() + Context.SiteTheme.Add(new SiteTheme() { Name = "Custom", NormalizedName = "Custom".ToNormalized(), @@ -110,16 +110,16 @@ public abstract class SiteThemeServiceTest : AbstractDbTest FileName = "custom.css", IsDefault = false }); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); - var customTheme = (await _unitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")); + var customTheme = (await UnitOfWork.SiteThemeRepository.GetThemeDtoByName("Custom")); Assert.NotNull(customTheme); await siteThemeService.UpdateDefault(customTheme.Id); - Assert.Equal(customTheme.Id, (await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Id); + Assert.Equal(customTheme.Id, (await UnitOfWork.SiteThemeRepository.GetDefaultTheme()).Id); } } diff --git a/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp b/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp new file mode 100644 index 000000000..0b46b66d2 Binary files /dev/null and b/API.Tests/Services/Test Data/CoverDbService/Existing/01.webp differ diff --git a/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp b/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp new file mode 100644 index 000000000..475824863 Binary files /dev/null and b/API.Tests/Services/Test Data/CoverDbService/Favicons/anilist.co.webp differ diff --git a/API.Tests/Services/VersionUpdaterServiceTests.cs b/API.Tests/Services/VersionUpdaterServiceTests.cs index c7a8a14d8..8be8f4aee 100644 --- a/API.Tests/Services/VersionUpdaterServiceTests.cs +++ b/API.Tests/Services/VersionUpdaterServiceTests.cs @@ -16,19 +16,15 @@ namespace API.Tests.Services; public class VersionUpdaterServiceTests : IDisposable { - private readonly ILogger _logger; - private readonly IEventHub _eventHub; - private readonly IDirectoryService _directoryService; + private readonly ILogger _logger = Substitute.For>(); + private readonly IEventHub _eventHub = Substitute.For(); + private readonly IDirectoryService _directoryService = Substitute.For(); private readonly VersionUpdaterService _service; private readonly string _tempPath; private readonly HttpTest _httpTest; public VersionUpdaterServiceTests() { - _logger = Substitute.For>(); - _eventHub = Substitute.For(); - _directoryService = Substitute.For(); - // Create temp directory for cache _tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(_tempPath); @@ -55,6 +51,7 @@ public class VersionUpdaterServiceTests : IDisposable // Reset BuildInfo.Version typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, null); + GC.SuppressFinalize(this); } [Fact] @@ -302,7 +299,7 @@ public class VersionUpdaterServiceTests : IDisposable var result = await _service.GetAllReleases(); - Assert.Equal(1, result.Count); + Assert.Single(result); Assert.Equal("0.7.0.0", result[0].UpdateVersion); Assert.NotEmpty(_httpTest.CallLog); // HTTP call was made } diff --git a/API.Tests/Services/WordCountAnalysisTests.cs b/API.Tests/Services/WordCountAnalysisTests.cs index 8c8c4193c..57c6ec7f6 100644 --- a/API.Tests/Services/WordCountAnalysisTests.cs +++ b/API.Tests/Services/WordCountAnalysisTests.cs @@ -26,9 +26,10 @@ public class WordCountAnalysisTests : AbstractDbTest private const long MinHoursToRead = 1; private const float AvgHoursToRead = 1.66954792f; private const long MaxHoursToRead = 3; - public WordCountAnalysisTests() : base() + + public WordCountAnalysisTests() { - _readerService = new ReaderService(_unitOfWork, Substitute.For>(), + _readerService = new ReaderService(UnitOfWork, Substitute.For>(), Substitute.For(), Substitute.For(), new DirectoryService(Substitute.For>(), new MockFileSystem()), Substitute.For()); @@ -36,9 +37,9 @@ public class WordCountAnalysisTests : AbstractDbTest protected override async Task ResetDb() { - _context.Series.RemoveRange(_context.Series.ToList()); + Context.Series.RemoveRange(Context.Series.ToList()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); } [Fact] @@ -56,7 +57,7 @@ public class WordCountAnalysisTests : AbstractDbTest MangaFormat.Epub).Build()) .Build(); - _context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test LIb", LibraryType.Book) .WithSeries(series) .Build()); @@ -67,11 +68,11 @@ public class WordCountAnalysisTests : AbstractDbTest .Build(), }; - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var cacheService = new CacheHelper(new FileService()); - var service = new WordCountAnalyzerService(Substitute.For>(), _unitOfWork, + var service = new WordCountAnalyzerService(Substitute.For>(), UnitOfWork, Substitute.For(), cacheService, _readerService, Substitute.For()); @@ -83,7 +84,7 @@ public class WordCountAnalysisTests : AbstractDbTest Assert.Equal(MaxHoursToRead, series.MaxHoursToRead); // Validate the Chapter gets updated correctly - var volume = series.Volumes.First(); + var volume = series.Volumes[0]; Assert.Equal(WordCount, volume.WordCount); Assert.Equal(MinHoursToRead, volume.MinHoursToRead); Assert.Equal(AvgHoursToRead, volume.AvgHoursToRead); @@ -114,16 +115,16 @@ public class WordCountAnalysisTests : AbstractDbTest .Build()) .Build(); - _context.Library.Add(new LibraryBuilder("Test", LibraryType.Book) + Context.Library.Add(new LibraryBuilder("Test", LibraryType.Book) .WithSeries(series) .Build()); - await _context.SaveChangesAsync(); + await Context.SaveChangesAsync(); var cacheService = new CacheHelper(new FileService()); - var service = new WordCountAnalyzerService(Substitute.For>(), _unitOfWork, + var service = new WordCountAnalyzerService(Substitute.For>(), UnitOfWork, Substitute.For(), cacheService, _readerService, Substitute.For()); await service.ScanSeries(1, 1); @@ -139,21 +140,21 @@ public class WordCountAnalysisTests : AbstractDbTest .WithChapter(chapter2) .Build()); - series.Volumes.First().Chapters.Add(chapter2); - await _unitOfWork.CommitAsync(); + series.Volumes[0].Chapters.Add(chapter2); + await UnitOfWork.CommitAsync(); await service.ScanSeries(1, 1); Assert.Equal(WordCount * 2L, series.WordCount); Assert.Equal(MinHoursToRead * 2, series.MinHoursToRead); - var firstVolume = series.Volumes.ElementAt(0); + var firstVolume = series.Volumes[0]; Assert.Equal(WordCount, firstVolume.WordCount); Assert.Equal(MinHoursToRead, firstVolume.MinHoursToRead); Assert.True(series.AvgHoursToRead.Is(AvgHoursToRead * 2)); Assert.Equal(MaxHoursToRead, firstVolume.MaxHoursToRead); - var secondVolume = series.Volumes.ElementAt(1); + var secondVolume = series.Volumes[1]; Assert.Equal(WordCount, secondVolume.WordCount); Assert.Equal(MinHoursToRead, secondVolume.MinHoursToRead); Assert.Equal(AvgHoursToRead, secondVolume.AvgHoursToRead); diff --git a/API/API.csproj b/API/API.csproj index 1ddb37d7f..f9a889d74 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -51,7 +51,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -66,7 +66,7 @@ - + @@ -78,7 +78,7 @@ - + @@ -87,20 +87,20 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + @@ -111,17 +111,16 @@ - - - - - + + + + @@ -139,6 +138,7 @@ + @@ -188,7 +188,6 @@ - Always diff --git a/API/Controllers/ChapterController.cs b/API/Controllers/ChapterController.cs index ca270d7a8..8de26cf97 100644 --- a/API/Controllers/ChapterController.cs +++ b/API/Controllers/ChapterController.cs @@ -68,7 +68,8 @@ public class ChapterController : BaseApiController { if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied")); - var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId); + var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId, + ChapterIncludes.Files | ChapterIncludes.ExternalReviews | ChapterIncludes.ExternalRatings); if (chapter == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist")); @@ -86,6 +87,15 @@ public class ChapterController : BaseApiController _unitOfWork.ChapterRepository.Remove(chapter); } + // If we removed the volume, do an additional check if we need to delete the actual series as well or not + var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(vol.SeriesId, SeriesIncludes.ExternalData | SeriesIncludes.Volumes); + var needToRemoveSeries = needToRemoveVolume && series != null && series.Volumes.Count <= 1; + if (needToRemoveSeries) + { + _unitOfWork.SeriesRepository.Remove(series!); + } + + if (!await _unitOfWork.CommitAsync()) return Ok(false); @@ -95,6 +105,12 @@ public class ChapterController : BaseApiController await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(chapter.VolumeId, vol.SeriesId), false); } + if (needToRemoveSeries) + { + await _eventHub.SendMessageAsync(MessageFactory.SeriesRemoved, + MessageFactory.SeriesRemovedEvent(series!.Id, series.Name, series.LibraryId), false); + } + return Ok(true); } @@ -419,7 +435,7 @@ public class ChapterController : BaseApiController ret.HasBeenRated = ownRating.HasBeenRated; } - var externalReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReviews(chapterId); + var externalReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReviewDtos(chapterId); if (externalReviews.Count > 0) { userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(externalReviews)); @@ -427,7 +443,7 @@ public class ChapterController : BaseApiController ret.Reviews = userReviews; - ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapterId); + ret.Ratings = await _unitOfWork.ChapterRepository.GetExternalChapterRatingDtos(chapterId); return Ok(ret); } diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 9757186bb..10a5f393a 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -6,9 +6,9 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.Data.Repositories; -using API.DTOs; using API.DTOs.Filtering; using API.DTOs.Metadata; +using API.DTOs.Person; using API.DTOs.Recommendation; using API.DTOs.SeriesDetail; using API.Entities.Enums; @@ -74,6 +74,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc { return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId(), ids)); } + return Ok(await unitOfWork.PersonRepository.GetAllPeopleDtosForLibrariesAsync(User.GetUserId())); } @@ -221,7 +222,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc return Ok(ret); } - private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto ret) + private async Task PrepareSeriesDetail(List userReviews, SeriesDetailPlusDto? ret) { var isAdmin = User.IsInRole(PolicyConstants.AdminRole); var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!; @@ -235,12 +236,12 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc ret.Recommendations.OwnedSeries = await unitOfWork.SeriesRepository.GetSeriesDtoByIdsAsync( ret.Recommendations.OwnedSeries.Select(s => s.Id), user); - ret.Recommendations.ExternalSeries = new List(); + ret.Recommendations.ExternalSeries = []; } if (ret.Recommendations != null && user != null) { - ret.Recommendations.OwnedSeries ??= new List(); + ret.Recommendations.OwnedSeries ??= []; await unitOfWork.SeriesRepository.AddSeriesModifiers(user.Id, ret.Recommendations.OwnedSeries); } } diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index fcc4ca58f..6e96c3063 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -15,6 +15,7 @@ using API.DTOs.CollectionTags; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; using API.DTOs.OPDS; +using API.DTOs.Person; using API.DTOs.Progress; using API.DTOs.Search; using API.Entities; diff --git a/API/Controllers/PersonController.cs b/API/Controllers/PersonController.cs index 1094a1137..a2ab3bf88 100644 --- a/API/Controllers/PersonController.cs +++ b/API/Controllers/PersonController.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.Data; +using API.Data.Repositories; using API.DTOs; +using API.DTOs.Person; using API.Entities.Enums; using API.Extensions; using API.Helpers; @@ -24,9 +27,10 @@ public class PersonController : BaseApiController private readonly ICoverDbService _coverDbService; private readonly IImageService _imageService; private readonly IEventHub _eventHub; + private readonly IPersonService _personService; public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper, - ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub) + ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService) { _unitOfWork = unitOfWork; _localizationService = localizationService; @@ -34,6 +38,7 @@ public class PersonController : BaseApiController _coverDbService = coverDbService; _imageService = imageService; _eventHub = eventHub; + _personService = personService; } @@ -43,6 +48,17 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetPersonDtoByName(name, User.GetUserId())); } + /// + /// Find a person by name or alias against a query string + /// + /// + /// + [HttpGet("search")] + public async Task>> SearchPeople([FromQuery] string queryString) + { + return Ok(await _unitOfWork.PersonRepository.SearchPeople(queryString)); + } + /// /// Returns all roles for a Person /// @@ -54,6 +70,7 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetRolesForPersonByName(personId, User.GetUserId())); } + /// /// Returns a list of authors and artists for browsing /// @@ -78,7 +95,7 @@ public class PersonController : BaseApiController public async Task> UpdatePerson(UpdatePersonDto dto) { // This needs to get all people and update them equally - var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id); + var person = await _unitOfWork.PersonRepository.GetPersonById(dto.Id, PersonIncludes.Aliases); if (person == null) return BadRequest(_localizationService.Translate(User.GetUserId(), "person-doesnt-exist")); if (string.IsNullOrEmpty(dto.Name)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-required")); @@ -90,6 +107,10 @@ public class PersonController : BaseApiController return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique")); } + var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases); + if (!success) return BadRequest(await _localizationService.Translate(User.GetUserId(), "aliases-have-overlap")); + + person.Name = dto.Name?.Trim(); person.Description = dto.Description ?? string.Empty; person.CoverImageLocked = dto.CoverImageLocked; @@ -173,5 +194,41 @@ public class PersonController : BaseApiController return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role)); } + /// + /// Merges Persons into one, this action is irreversible + /// + /// + /// + [HttpPost("merge")] + public async Task> MergePeople(PersonMergeDto dto) + { + var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId, PersonIncludes.All); + if (dst == null) return BadRequest(); + + var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId, PersonIncludes.All); + if (src == null) return BadRequest(); + + await _personService.MergePeopleAsync(src, dst); + await _eventHub.SendMessageAsync(MessageFactory.PersonMerged, MessageFactory.PersonMergedMessage(dst, src)); + + return Ok(_mapper.Map(dst)); + } + + /// + /// Ensure the alias is valid to be added. For example, the alias cannot be on another person or be the same as the current person name/alias. + /// + /// + /// + /// + [HttpGet("valid-alias")] + public async Task> IsValidAlias(int personId, string alias) + { + var person = await _unitOfWork.PersonRepository.GetPersonById(personId, PersonIncludes.Aliases); + if (person == null) return NotFound(); + + var existingAlias = await _unitOfWork.PersonRepository.AnyAliasExist(alias); + return Ok(!existingAlias && person.NormalizedName != alias.ToNormalized()); + } + } diff --git a/API/Controllers/PluginController.cs b/API/Controllers/PluginController.cs index 87cfaf2c2..c7f48cf54 100644 --- a/API/Controllers/PluginController.cs +++ b/API/Controllers/PluginController.cs @@ -30,7 +30,7 @@ public class PluginController(IUnitOfWork unitOfWork, ITokenService tokenService public async Task> Authenticate([Required] string apiKey, [Required] string pluginName) { // NOTE: In order to log information about plugins, we need some Plugin Description information for each request - // Should log into access table so we can tell the user + // Should log into the access table so we can tell the user var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = HttpContext.Request.Headers.UserAgent; var userId = await unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs index 6c9be6c75..1187992bc 100644 --- a/API/Controllers/ReadingListController.cs +++ b/API/Controllers/ReadingListController.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using API.Constants; using API.Data; using API.Data.Repositories; -using API.DTOs; +using API.DTOs.Person; using API.DTOs.ReadingLists; using API.Entities.Enums; using API.Extensions; diff --git a/API/Controllers/SearchController.cs b/API/Controllers/SearchController.cs index 5aa54d1db..cc89a124e 100644 --- a/API/Controllers/SearchController.cs +++ b/API/Controllers/SearchController.cs @@ -63,6 +63,7 @@ public class SearchController : BaseApiController var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); if (user == null) return Unauthorized(); + var libraries = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(user.Id, QueryContext.Search).ToList(); if (libraries.Count == 0) return BadRequest(await _localizationService.Translate(User.GetUserId(), "libraries-restricted")); diff --git a/API/DTOs/Account/AgeRestrictionDto.cs b/API/DTOs/Account/AgeRestrictionDto.cs index 0aaec9b97..6505bdbff 100644 --- a/API/DTOs/Account/AgeRestrictionDto.cs +++ b/API/DTOs/Account/AgeRestrictionDto.cs @@ -2,15 +2,15 @@ namespace API.DTOs.Account; -public class AgeRestrictionDto +public sealed record AgeRestrictionDto { /// /// The maximum age rating a user has access to. -1 if not applicable /// - public required AgeRating AgeRating { get; set; } = AgeRating.NotApplicable; + public required AgeRating AgeRating { get; init; } = AgeRating.NotApplicable; /// /// Are Unknowns explicitly allowed against age rating /// /// Unknown is always lowest and default age rating. Setting this to false will ensure Teen age rating applies and unknowns are still filtered - public required bool IncludeUnknowns { get; set; } = false; + public required bool IncludeUnknowns { get; init; } = false; } diff --git a/API/DTOs/Account/ConfirmEmailDto.cs b/API/DTOs/Account/ConfirmEmailDto.cs index 2f5849e74..413f9f34a 100644 --- a/API/DTOs/Account/ConfirmEmailDto.cs +++ b/API/DTOs/Account/ConfirmEmailDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Account; -public class ConfirmEmailDto +public sealed record ConfirmEmailDto { [Required] public string Email { get; set; } = default!; diff --git a/API/DTOs/Account/ConfirmEmailUpdateDto.cs b/API/DTOs/Account/ConfirmEmailUpdateDto.cs index 42abb1295..2a0738e35 100644 --- a/API/DTOs/Account/ConfirmEmailUpdateDto.cs +++ b/API/DTOs/Account/ConfirmEmailUpdateDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Account; -public class ConfirmEmailUpdateDto +public sealed record ConfirmEmailUpdateDto { [Required] public string Email { get; set; } = default!; diff --git a/API/DTOs/Account/ConfirmMigrationEmailDto.cs b/API/DTOs/Account/ConfirmMigrationEmailDto.cs index efb42b8fd..cdfc1505c 100644 --- a/API/DTOs/Account/ConfirmMigrationEmailDto.cs +++ b/API/DTOs/Account/ConfirmMigrationEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public class ConfirmMigrationEmailDto +public sealed record ConfirmMigrationEmailDto { public string Email { get; set; } = default!; public string Token { get; set; } = default!; diff --git a/API/DTOs/Account/ConfirmPasswordResetDto.cs b/API/DTOs/Account/ConfirmPasswordResetDto.cs index 16dd86f9a..00aff301b 100644 --- a/API/DTOs/Account/ConfirmPasswordResetDto.cs +++ b/API/DTOs/Account/ConfirmPasswordResetDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Account; -public class ConfirmPasswordResetDto +public sealed record ConfirmPasswordResetDto { [Required] public string Email { get; set; } = default!; diff --git a/API/DTOs/Account/InviteUserDto.cs b/API/DTOs/Account/InviteUserDto.cs index 112013053..c12bebc2b 100644 --- a/API/DTOs/Account/InviteUserDto.cs +++ b/API/DTOs/Account/InviteUserDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; -public class InviteUserDto +public sealed record InviteUserDto { [Required] public string Email { get; set; } = default!; diff --git a/API/DTOs/Account/InviteUserResponse.cs b/API/DTOs/Account/InviteUserResponse.cs index a7e0d86ea..ed16bd05e 100644 --- a/API/DTOs/Account/InviteUserResponse.cs +++ b/API/DTOs/Account/InviteUserResponse.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public class InviteUserResponse +public sealed record InviteUserResponse { /// /// Email link used to setup the user account diff --git a/API/DTOs/Account/LoginDto.cs b/API/DTOs/Account/LoginDto.cs index fe8fce088..97338640b 100644 --- a/API/DTOs/Account/LoginDto.cs +++ b/API/DTOs/Account/LoginDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs.Account; #nullable enable -public class LoginDto +public sealed record LoginDto { public string Username { get; init; } = default!; public string Password { get; set; } = default!; diff --git a/API/DTOs/Account/MigrateUserEmailDto.cs b/API/DTOs/Account/MigrateUserEmailDto.cs index 60d042165..4630c510f 100644 --- a/API/DTOs/Account/MigrateUserEmailDto.cs +++ b/API/DTOs/Account/MigrateUserEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public class MigrateUserEmailDto +public sealed record MigrateUserEmailDto { public string Email { get; set; } = default!; public string Username { get; set; } = default!; diff --git a/API/DTOs/Account/ResetPasswordDto.cs b/API/DTOs/Account/ResetPasswordDto.cs index 51a195131..545ca5ba6 100644 --- a/API/DTOs/Account/ResetPasswordDto.cs +++ b/API/DTOs/Account/ResetPasswordDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Account; -public class ResetPasswordDto +public sealed record ResetPasswordDto { /// /// The Username of the User diff --git a/API/DTOs/Account/TokenRequestDto.cs b/API/DTOs/Account/TokenRequestDto.cs index 85ab9f87a..5c798721c 100644 --- a/API/DTOs/Account/TokenRequestDto.cs +++ b/API/DTOs/Account/TokenRequestDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public class TokenRequestDto +public sealed record TokenRequestDto { public string Token { get; init; } = default!; public string RefreshToken { get; init; } = default!; diff --git a/API/DTOs/Account/UpdateAgeRestrictionDto.cs b/API/DTOs/Account/UpdateAgeRestrictionDto.cs index ef6be1bba..2fa9c89d2 100644 --- a/API/DTOs/Account/UpdateAgeRestrictionDto.cs +++ b/API/DTOs/Account/UpdateAgeRestrictionDto.cs @@ -3,7 +3,7 @@ using API.Entities.Enums; namespace API.DTOs.Account; -public class UpdateAgeRestrictionDto +public sealed record UpdateAgeRestrictionDto { [Required] public AgeRating AgeRating { get; set; } diff --git a/API/DTOs/Account/UpdateEmailDto.cs b/API/DTOs/Account/UpdateEmailDto.cs index eac06be53..873862ba1 100644 --- a/API/DTOs/Account/UpdateEmailDto.cs +++ b/API/DTOs/Account/UpdateEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Account; -public class UpdateEmailDto +public sealed record UpdateEmailDto { public string Email { get; set; } = default!; public string Password { get; set; } = default!; diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index c40124b7b..0cb0eaf66 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -4,12 +4,16 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Account; #nullable enable -public record UpdateUserDto +public sealed record UpdateUserDto { + /// public int UserId { get; set; } + /// public string Username { get; set; } = default!; + /// /// List of Roles to assign to user. If admin not present, Pleb will be applied. /// If admin present, all libraries will be granted access and will ignore those from DTO. + /// public IList Roles { get; init; } = default!; /// /// A list of libraries to grant access to @@ -19,8 +23,6 @@ public record UpdateUserDto /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// public AgeRestrictionDto AgeRestriction { get; init; } = default!; - /// - /// Email of the user - /// + /// public string? Email { get; set; } = default!; } diff --git a/API/DTOs/BulkActionDto.cs b/API/DTOs/BulkActionDto.cs index d3ce75293..c26a73e9c 100644 --- a/API/DTOs/BulkActionDto.cs +++ b/API/DTOs/BulkActionDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public class BulkActionDto +public sealed record BulkActionDto { public List Ids { get; set; } /** diff --git a/API/DTOs/ChapterDetailPlusDto.cs b/API/DTOs/ChapterDetailPlusDto.cs index 9f9cfb8ab..d99482e55 100644 --- a/API/DTOs/ChapterDetailPlusDto.cs +++ b/API/DTOs/ChapterDetailPlusDto.cs @@ -4,7 +4,7 @@ using API.DTOs.SeriesDetail; namespace API.DTOs; -public class ChapterDetailPlusDto +public sealed record ChapterDetailPlusDto { public float Rating { get; set; } public bool HasBeenRated { get; set; } diff --git a/API/DTOs/ChapterDto.cs b/API/DTOs/ChapterDto.cs index 70c77e92d..85624b51c 100644 --- a/API/DTOs/ChapterDto.cs +++ b/API/DTOs/ChapterDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using API.DTOs.Metadata; +using API.DTOs.Person; using API.Entities.Enums; using API.Entities.Interfaces; @@ -13,37 +14,24 @@ namespace API.DTOs; /// public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage { + /// public int Id { get; init; } - /// - /// Range of chapters. Chapter 2-4 -> "2-4". Chapter 2 -> "2". If special, will be special name. - /// - /// This can be something like 19.HU or Alpha as some comics are like this + /// public string Range { get; init; } = default!; - /// - /// Smallest number of the Range. - /// + /// [Obsolete("Use MinNumber and MaxNumber instead")] public string Number { get; init; } = default!; - /// - /// This may be 0 under the circumstance that the Issue is "Alpha" or other non-standard numbers. - /// + /// public float MinNumber { get; init; } + /// public float MaxNumber { get; init; } - /// - /// The sorting order of the Chapter. Inherits from MinNumber, but can be overridden. - /// + /// public float SortOrder { get; set; } - /// - /// Total number of pages in all MangaFiles - /// + /// public int Pages { get; init; } - /// - /// If this Chapter contains files that could only be identified as Series or has Special Identifier from filename - /// + /// public bool IsSpecial { get; init; } - /// - /// Used for books/specials to display custom title. For non-specials/books, will be set to - /// + /// public string Title { get; set; } = default!; /// /// The files that represent this Chapter @@ -61,46 +49,25 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// The last time a chapter was read by current authenticated user /// public DateTime LastReadingProgress { get; set; } - /// - /// If the Cover Image is locked for this entity - /// + /// public bool CoverImageLocked { get; set; } - /// - /// Volume Id this Chapter belongs to - /// + /// public int VolumeId { get; init; } - /// - /// When chapter was created - /// + /// public DateTime CreatedUtc { get; set; } + /// public DateTime LastModifiedUtc { get; set; } - /// - /// When chapter was created in local server time - /// - /// This is required for Tachiyomi Extension + /// public DateTime Created { get; set; } - /// - /// When the chapter was released. - /// - /// Metadata field + /// public DateTime ReleaseDate { get; init; } - /// - /// Title of the Chapter/Issue - /// - /// Metadata field + /// public string TitleName { get; set; } = default!; - /// - /// Summary of the Chapter - /// - /// This is not set normally, only for Series Detail + /// public string Summary { get; init; } = default!; - /// - /// Age Rating for the issue/chapter - /// + /// public AgeRating AgeRating { get; init; } - /// - /// Total words in a Chapter (books only) - /// + /// public long WordCount { get; set; } = 0L; /// /// Formatted Volume title ie) Volume 2. @@ -113,14 +80,9 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage public int MaxHoursToRead { get; set; } /// public float AvgHoursToRead { get; set; } - /// - /// Comma-separated link of urls to external services that have some relation to the Chapter - /// + /// public string WebLinks { get; set; } - /// - /// ISBN-13 (usually) of the Chapter - /// - /// This is guaranteed to be Valid + /// public string ISBN { get; set; } #region Metadata @@ -146,51 +108,60 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage /// public ICollection Tags { get; set; } = new List(); public PublicationStatus PublicationStatus { get; set; } - /// - /// Language for the Chapter/Issue - /// + /// public string? Language { get; set; } - /// - /// Number in the TotalCount of issues - /// + /// public int Count { get; set; } - /// - /// Total number of issues for the series - /// + /// public int TotalCount { get; set; } + /// public bool LanguageLocked { get; set; } + /// public bool SummaryLocked { get; set; } - /// - /// Locked by user so metadata updates from scan loop will not override AgeRating - /// + /// public bool AgeRatingLocked { get; set; } - /// - /// Locked by user so metadata updates from scan loop will not override PublicationStatus - /// public bool PublicationStatusLocked { get; set; } + /// public bool GenresLocked { get; set; } + /// public bool TagsLocked { get; set; } + /// public bool WriterLocked { get; set; } + /// public bool CharacterLocked { get; set; } + /// 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; } + /// public bool TranslatorLocked { get; set; } + /// public bool TeamLocked { get; set; } + /// public bool LocationLocked { get; set; } + /// public bool CoverArtistLocked { get; set; } public bool ReleaseYearLocked { get; set; } #endregion - public string CoverImage { get; set; } - public string PrimaryColor { get; set; } = string.Empty; - public string SecondaryColor { get; set; } = string.Empty; + /// + public string? CoverImage { get; set; } + /// + public string? PrimaryColor { get; set; } = string.Empty; + /// + public string? SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/Collection/AppUserCollectionDto.cs b/API/DTOs/Collection/AppUserCollectionDto.cs index ecfb5c062..0634b5d83 100644 --- a/API/DTOs/Collection/AppUserCollectionDto.cs +++ b/API/DTOs/Collection/AppUserCollectionDto.cs @@ -6,52 +6,52 @@ using API.Services.Plus; namespace API.DTOs.Collection; #nullable enable -public class AppUserCollectionDto : IHasCoverImage +public sealed record AppUserCollectionDto : IHasCoverImage { public int Id { get; init; } - public string Title { get; set; } = default!; - public string? Summary { get; set; } = default!; - public bool Promoted { get; set; } - public AgeRating AgeRating { get; set; } + public string Title { get; init; } = default!; + public string? Summary { get; init; } = default!; + public bool Promoted { get; init; } + public AgeRating AgeRating { get; init; } /// /// This is used to tell the UI if it should request a Cover Image or not. If null or empty, it has not been set. /// public string? CoverImage { get; set; } = string.Empty; - public string PrimaryColor { get; set; } = string.Empty; - public string SecondaryColor { get; set; } = string.Empty; - public bool CoverImageLocked { get; set; } + public string? PrimaryColor { get; set; } = string.Empty; + public string? SecondaryColor { get; set; } = string.Empty; + public bool CoverImageLocked { get; init; } /// /// Number of Series in the Collection /// - public int ItemCount { get; set; } + public int ItemCount { get; init; } /// /// Owner of the Collection /// - public string? Owner { get; set; } + public string? Owner { get; init; } /// /// Last time Kavita Synced the Collection with an upstream source (for non Kavita sourced collections) /// - public DateTime LastSyncUtc { get; set; } + public DateTime LastSyncUtc { get; init; } /// /// Who created/manages the list. Non-Kavita lists are not editable by the user, except to promote /// - public ScrobbleProvider Source { get; set; } = ScrobbleProvider.Kavita; + public ScrobbleProvider Source { get; init; } = ScrobbleProvider.Kavita; /// /// For Non-Kavita sourced collections, the url to sync from /// - public string? SourceUrl { get; set; } + public string? SourceUrl { get; init; } /// /// Total number of items as of the last sync. Not applicable for Kavita managed collections. /// - public int TotalSourceCount { get; set; } + public int TotalSourceCount { get; init; } /// /// A
separated string of all missing series ///
- public string? MissingSeriesFromSource { get; set; } + public string? MissingSeriesFromSource { get; init; } public void ResetColorScape() { diff --git a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs b/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs index 1d078959d..0a2270fbf 100644 --- a/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs +++ b/API/DTOs/CollectionTags/CollectionTagBulkAddDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.CollectionTags; -public class CollectionTagBulkAddDto +public sealed record CollectionTagBulkAddDto { /// /// Collection Tag Id diff --git a/API/DTOs/CollectionTags/CollectionTagDto.cs b/API/DTOs/CollectionTags/CollectionTagDto.cs index ec9939ebd..911622051 100644 --- a/API/DTOs/CollectionTags/CollectionTagDto.cs +++ b/API/DTOs/CollectionTags/CollectionTagDto.cs @@ -3,15 +3,21 @@ namespace API.DTOs.CollectionTags; [Obsolete("Use AppUserCollectionDto")] -public class CollectionTagDto +public sealed record CollectionTagDto { + /// public int Id { get; set; } + /// public string Title { get; set; } = default!; + /// public string Summary { get; set; } = default!; + /// public bool Promoted { get; set; } /// /// The cover image string. This is used on Frontend to show or hide the Cover Image /// + /// public string CoverImage { get; set; } = default!; + /// public bool CoverImageLocked { get; set; } } diff --git a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs index 19e9a11e2..139834a60 100644 --- a/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs +++ b/API/DTOs/CollectionTags/UpdateSeriesForTagDto.cs @@ -4,7 +4,7 @@ using API.DTOs.Collection; namespace API.DTOs.CollectionTags; -public class UpdateSeriesForTagDto +public sealed record UpdateSeriesForTagDto { public AppUserCollectionDto Tag { get; init; } = default!; public IEnumerable SeriesIdsToRemove { get; init; } = default!; diff --git a/API/DTOs/ColorScape.cs b/API/DTOs/ColorScape.cs index d95346af7..5351f2351 100644 --- a/API/DTOs/ColorScape.cs +++ b/API/DTOs/ColorScape.cs @@ -4,7 +4,7 @@ /// /// A primary and secondary color /// -public class ColorScape +public sealed record ColorScape { public required string? Primary { get; set; } public required string? Secondary { get; set; } diff --git a/API/DTOs/CopySettingsFromLibraryDto.cs b/API/DTOs/CopySettingsFromLibraryDto.cs index ee75f7422..5ca5ead51 100644 --- a/API/DTOs/CopySettingsFromLibraryDto.cs +++ b/API/DTOs/CopySettingsFromLibraryDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public class CopySettingsFromLibraryDto +public sealed record CopySettingsFromLibraryDto { public int SourceLibraryId { get; set; } public List TargetLibraryIds { get; set; } diff --git a/API/DTOs/CoverDb/CoverDbAuthor.cs b/API/DTOs/CoverDb/CoverDbAuthor.cs index 2f023398a..ca924801f 100644 --- a/API/DTOs/CoverDb/CoverDbAuthor.cs +++ b/API/DTOs/CoverDb/CoverDbAuthor.cs @@ -3,7 +3,7 @@ using YamlDotNet.Serialization; namespace API.DTOs.CoverDb; -public class CoverDbAuthor +public sealed record CoverDbAuthor { [YamlMember(Alias = "name", ApplyNamingConventions = false)] public string Name { get; set; } diff --git a/API/DTOs/CoverDb/CoverDbPeople.cs b/API/DTOs/CoverDb/CoverDbPeople.cs index c0f5e327e..2e825eac7 100644 --- a/API/DTOs/CoverDb/CoverDbPeople.cs +++ b/API/DTOs/CoverDb/CoverDbPeople.cs @@ -3,7 +3,7 @@ using YamlDotNet.Serialization; namespace API.DTOs.CoverDb; -public class CoverDbPeople +public sealed record CoverDbPeople { [YamlMember(Alias = "people", ApplyNamingConventions = false)] public List People { get; set; } = new List(); diff --git a/API/DTOs/CoverDb/CoverDbPersonIds.cs b/API/DTOs/CoverDb/CoverDbPersonIds.cs index 9c59415e6..5816bb479 100644 --- a/API/DTOs/CoverDb/CoverDbPersonIds.cs +++ b/API/DTOs/CoverDb/CoverDbPersonIds.cs @@ -3,7 +3,7 @@ namespace API.DTOs.CoverDb; #nullable enable -public class CoverDbPersonIds +public sealed record CoverDbPersonIds { [YamlMember(Alias = "hardcover_id", ApplyNamingConventions = false)] public string? HardcoverId { get; set; } = null; diff --git a/API/DTOs/Dashboard/DashboardStreamDto.cs b/API/DTOs/Dashboard/DashboardStreamDto.cs index 59e5f4f7d..297a706b1 100644 --- a/API/DTOs/Dashboard/DashboardStreamDto.cs +++ b/API/DTOs/Dashboard/DashboardStreamDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.Dashboard; -public class DashboardStreamDto +public sealed record DashboardStreamDto { public int Id { get; set; } public required string Name { get; set; } diff --git a/API/DTOs/Dashboard/GroupedSeriesDto.cs b/API/DTOs/Dashboard/GroupedSeriesDto.cs index 3b283de34..940e42c40 100644 --- a/API/DTOs/Dashboard/GroupedSeriesDto.cs +++ b/API/DTOs/Dashboard/GroupedSeriesDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Dashboard; /// /// This is a representation of a Series with some amount of underlying files within it. This is used for Recently Updated Series section /// -public class GroupedSeriesDto +public sealed record GroupedSeriesDto { public string SeriesName { get; set; } = default!; public int SeriesId { get; set; } diff --git a/API/DTOs/Dashboard/RecentlyAddedItemDto.cs b/API/DTOs/Dashboard/RecentlyAddedItemDto.cs index 2e5658e2e..bb0360b30 100644 --- a/API/DTOs/Dashboard/RecentlyAddedItemDto.cs +++ b/API/DTOs/Dashboard/RecentlyAddedItemDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Dashboard; /// /// A mesh of data for Recently added volume/chapters /// -public class RecentlyAddedItemDto +public sealed record RecentlyAddedItemDto { public string SeriesName { get; set; } = default!; public int SeriesId { get; set; } diff --git a/API/DTOs/Dashboard/SmartFilterDto.cs b/API/DTOs/Dashboard/SmartFilterDto.cs index b23a74c69..c1bc4d7e1 100644 --- a/API/DTOs/Dashboard/SmartFilterDto.cs +++ b/API/DTOs/Dashboard/SmartFilterDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Dashboard; -public class SmartFilterDto +public sealed record SmartFilterDto { public int Id { get; set; } public required string Name { get; set; } diff --git a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs b/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs index c2320f1a9..476a0732e 100644 --- a/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs +++ b/API/DTOs/Dashboard/UpdateDashboardStreamPositionDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Dashboard; -public class UpdateDashboardStreamPositionDto +public sealed record UpdateDashboardStreamPositionDto { public int FromPosition { get; set; } public int ToPosition { get; set; } diff --git a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs b/API/DTOs/Dashboard/UpdateStreamPositionDto.cs index f9005a585..8de0ffa6f 100644 --- a/API/DTOs/Dashboard/UpdateStreamPositionDto.cs +++ b/API/DTOs/Dashboard/UpdateStreamPositionDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Dashboard; -public class UpdateStreamPositionDto +public sealed record UpdateStreamPositionDto { public int FromPosition { get; set; } public int ToPosition { get; set; } diff --git a/API/DTOs/DeleteChaptersDto.cs b/API/DTOs/DeleteChaptersDto.cs index cbd21df36..9fad2f1fb 100644 --- a/API/DTOs/DeleteChaptersDto.cs +++ b/API/DTOs/DeleteChaptersDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public class DeleteChaptersDto +public sealed record DeleteChaptersDto { public IList ChapterIds { get; set; } = default!; } diff --git a/API/DTOs/DeleteSeriesDto.cs b/API/DTOs/DeleteSeriesDto.cs index 12687fc25..ec9ba0c68 100644 --- a/API/DTOs/DeleteSeriesDto.cs +++ b/API/DTOs/DeleteSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public class DeleteSeriesDto +public sealed record DeleteSeriesDto { public IList SeriesIds { get; set; } = default!; } diff --git a/API/DTOs/Device/CreateDeviceDto.cs b/API/DTOs/Device/CreateDeviceDto.cs index 7e59483fa..a8fdb6bc9 100644 --- a/API/DTOs/Device/CreateDeviceDto.cs +++ b/API/DTOs/Device/CreateDeviceDto.cs @@ -3,7 +3,7 @@ using API.Entities.Enums.Device; namespace API.DTOs.Device; -public class CreateDeviceDto +public sealed record CreateDeviceDto { [Required] public string Name { get; set; } = default!; diff --git a/API/DTOs/Device/DeviceDto.cs b/API/DTOs/Device/DeviceDto.cs index b2e83e6fc..42140dcc1 100644 --- a/API/DTOs/Device/DeviceDto.cs +++ b/API/DTOs/Device/DeviceDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Device; /// /// A Device is an entity that can receive data from Kavita (kindle) /// -public class DeviceDto +public sealed record DeviceDto { /// /// The device Id diff --git a/API/DTOs/Device/SendSeriesToDeviceDto.cs b/API/DTOs/Device/SendSeriesToDeviceDto.cs index a0a907464..58ce2293b 100644 --- a/API/DTOs/Device/SendSeriesToDeviceDto.cs +++ b/API/DTOs/Device/SendSeriesToDeviceDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Device; -public class SendSeriesToDeviceDto +public sealed record SendSeriesToDeviceDto { public int DeviceId { get; set; } public int SeriesId { get; set; } diff --git a/API/DTOs/Device/SendToDeviceDto.cs b/API/DTOs/Device/SendToDeviceDto.cs index fd88eaf59..a7a4dc0ff 100644 --- a/API/DTOs/Device/SendToDeviceDto.cs +++ b/API/DTOs/Device/SendToDeviceDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Device; -public class SendToDeviceDto +public sealed record SendToDeviceDto { public int DeviceId { get; set; } public IReadOnlyList ChapterIds { get; set; } = default!; diff --git a/API/DTOs/Device/UpdateDeviceDto.cs b/API/DTOs/Device/UpdateDeviceDto.cs index d28d372c3..2c3e72ea1 100644 --- a/API/DTOs/Device/UpdateDeviceDto.cs +++ b/API/DTOs/Device/UpdateDeviceDto.cs @@ -3,7 +3,7 @@ using API.Entities.Enums.Device; namespace API.DTOs.Device; -public class UpdateDeviceDto +public sealed record UpdateDeviceDto { [Required] public int Id { get; set; } diff --git a/API/DTOs/Downloads/DownloadBookmarkDto.cs b/API/DTOs/Downloads/DownloadBookmarkDto.cs index 5b7240b68..00f763dac 100644 --- a/API/DTOs/Downloads/DownloadBookmarkDto.cs +++ b/API/DTOs/Downloads/DownloadBookmarkDto.cs @@ -4,7 +4,7 @@ using API.DTOs.Reader; namespace API.DTOs.Downloads; -public class DownloadBookmarkDto +public sealed record DownloadBookmarkDto { [Required] public IEnumerable Bookmarks { get; set; } = default!; diff --git a/API/DTOs/Email/ConfirmationEmailDto.cs b/API/DTOs/Email/ConfirmationEmailDto.cs index 1a48c9974..197395794 100644 --- a/API/DTOs/Email/ConfirmationEmailDto.cs +++ b/API/DTOs/Email/ConfirmationEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public class ConfirmationEmailDto +public sealed record ConfirmationEmailDto { public string InvitingUser { get; init; } = default!; public string EmailAddress { get; init; } = default!; diff --git a/API/DTOs/Email/EmailHistoryDto.cs b/API/DTOs/Email/EmailHistoryDto.cs index ca3549550..c2968d091 100644 --- a/API/DTOs/Email/EmailHistoryDto.cs +++ b/API/DTOs/Email/EmailHistoryDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Email; -public class EmailHistoryDto +public sealed record EmailHistoryDto { public long Id { get; set; } public bool Sent { get; set; } diff --git a/API/DTOs/Email/EmailMigrationDto.cs b/API/DTOs/Email/EmailMigrationDto.cs index f051e7337..5354afdaa 100644 --- a/API/DTOs/Email/EmailMigrationDto.cs +++ b/API/DTOs/Email/EmailMigrationDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public class EmailMigrationDto +public sealed record EmailMigrationDto { public string EmailAddress { get; init; } = default!; public string Username { get; init; } = default!; diff --git a/API/DTOs/Email/EmailTestResultDto.cs b/API/DTOs/Email/EmailTestResultDto.cs index 263e725c4..9be868eab 100644 --- a/API/DTOs/Email/EmailTestResultDto.cs +++ b/API/DTOs/Email/EmailTestResultDto.cs @@ -3,7 +3,7 @@ /// /// Represents if Test Email Service URL was successful or not and if any error occured /// -public class EmailTestResultDto +public sealed record EmailTestResultDto { public bool Successful { get; set; } public string ErrorMessage { get; set; } = default!; diff --git a/API/DTOs/Email/PasswordResetEmailDto.cs b/API/DTOs/Email/PasswordResetEmailDto.cs index 06abba171..9fda066a9 100644 --- a/API/DTOs/Email/PasswordResetEmailDto.cs +++ b/API/DTOs/Email/PasswordResetEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public class PasswordResetEmailDto +public sealed record PasswordResetEmailDto { public string EmailAddress { get; init; } = default!; public string ServerConfirmationLink { get; init; } = default!; diff --git a/API/DTOs/Email/SendToDto.cs b/API/DTOs/Email/SendToDto.cs index 1261d110c..eacd29449 100644 --- a/API/DTOs/Email/SendToDto.cs +++ b/API/DTOs/Email/SendToDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Email; -public class SendToDto +public sealed record SendToDto { public string DestinationEmail { get; set; } = default!; public IEnumerable FilePaths { get; set; } = default!; diff --git a/API/DTOs/Email/TestEmailDto.cs b/API/DTOs/Email/TestEmailDto.cs index 37c12ed30..44c11bd6c 100644 --- a/API/DTOs/Email/TestEmailDto.cs +++ b/API/DTOs/Email/TestEmailDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Email; -public class TestEmailDto +public sealed record TestEmailDto { public string Url { get; set; } = default!; } diff --git a/API/DTOs/Filtering/FilterDto.cs b/API/DTOs/Filtering/FilterDto.cs index 9205a7bba..cb3374838 100644 --- a/API/DTOs/Filtering/FilterDto.cs +++ b/API/DTOs/Filtering/FilterDto.cs @@ -5,7 +5,7 @@ using API.Entities.Enums; namespace API.DTOs.Filtering; #nullable enable -public class FilterDto +public sealed record FilterDto { /// /// The type of Formats you want to be returned. An empty list will return all formats back diff --git a/API/DTOs/Filtering/LanguageDto.cs b/API/DTOs/Filtering/LanguageDto.cs index bc7ebb5cc..dde85f07e 100644 --- a/API/DTOs/Filtering/LanguageDto.cs +++ b/API/DTOs/Filtering/LanguageDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Filtering; -public class LanguageDto +public sealed record LanguageDto { public required string IsoCode { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Filtering/Range.cs b/API/DTOs/Filtering/Range.cs index a75164fa3..e697f26e1 100644 --- a/API/DTOs/Filtering/Range.cs +++ b/API/DTOs/Filtering/Range.cs @@ -4,7 +4,7 @@ /// /// Represents a range between two int/float/double /// -public class Range +public sealed record Range { public T? Min { get; init; } public T? Max { get; init; } diff --git a/API/DTOs/Filtering/ReadStatus.cs b/API/DTOs/Filtering/ReadStatus.cs index eeb786714..81498ecb5 100644 --- a/API/DTOs/Filtering/ReadStatus.cs +++ b/API/DTOs/Filtering/ReadStatus.cs @@ -3,7 +3,7 @@ /// /// Represents the Reading Status. This is a flag and allows multiple statues /// -public class ReadStatus +public sealed record ReadStatus { public bool NotRead { get; set; } = true; public bool InProgress { get; set; } = true; diff --git a/API/DTOs/Filtering/SortOptions.cs b/API/DTOs/Filtering/SortOptions.cs index 00bf91675..a08e2968e 100644 --- a/API/DTOs/Filtering/SortOptions.cs +++ b/API/DTOs/Filtering/SortOptions.cs @@ -3,7 +3,7 @@ /// /// Sorting Options for a query /// -public class SortOptions +public sealed record SortOptions { public SortField SortField { get; set; } public bool IsAscending { get; set; } = true; diff --git a/API/DTOs/Filtering/v2/DecodeFilterDto.cs b/API/DTOs/Filtering/v2/DecodeFilterDto.cs index 18dc166e7..db4c7ecce 100644 --- a/API/DTOs/Filtering/v2/DecodeFilterDto.cs +++ b/API/DTOs/Filtering/v2/DecodeFilterDto.cs @@ -3,7 +3,7 @@ /// /// For requesting an encoded filter to be decoded /// -public class DecodeFilterDto +public sealed record DecodeFilterDto { public string EncodedFilter { get; set; } } diff --git a/API/DTOs/Filtering/v2/FilterStatementDto.cs b/API/DTOs/Filtering/v2/FilterStatementDto.cs index a6192093e..ebe6d16af 100644 --- a/API/DTOs/Filtering/v2/FilterStatementDto.cs +++ b/API/DTOs/Filtering/v2/FilterStatementDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Filtering.v2; -public class FilterStatementDto +public sealed record FilterStatementDto { public FilterComparison Comparison { get; set; } public FilterField Field { get; set; } diff --git a/API/DTOs/Filtering/v2/FilterV2Dto.cs b/API/DTOs/Filtering/v2/FilterV2Dto.cs index 5bc50ff2f..11dc42a6b 100644 --- a/API/DTOs/Filtering/v2/FilterV2Dto.cs +++ b/API/DTOs/Filtering/v2/FilterV2Dto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Filtering.v2; /// /// Metadata filtering for v2 API only /// -public class FilterV2Dto +public sealed record FilterV2Dto { /// /// Not used in the UI. diff --git a/API/DTOs/Jobs/JobDto.cs b/API/DTOs/Jobs/JobDto.cs index 648765a34..55419811f 100644 --- a/API/DTOs/Jobs/JobDto.cs +++ b/API/DTOs/Jobs/JobDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Jobs; -public class JobDto +public sealed record JobDto { /// /// Job Id diff --git a/API/DTOs/JumpBar/JumpKeyDto.cs b/API/DTOs/JumpBar/JumpKeyDto.cs index 5a98a85ca..8dc5b4a8e 100644 --- a/API/DTOs/JumpBar/JumpKeyDto.cs +++ b/API/DTOs/JumpBar/JumpKeyDto.cs @@ -3,7 +3,7 @@ /// /// Represents an individual button in a Jump Bar /// -public class JumpKeyDto +public sealed record JumpKeyDto { /// /// Number of items in this Key diff --git a/API/DTOs/KavitaLocale.cs b/API/DTOs/KavitaLocale.cs index decfb7395..51868605f 100644 --- a/API/DTOs/KavitaLocale.cs +++ b/API/DTOs/KavitaLocale.cs @@ -1,6 +1,6 @@ namespace API.DTOs; -public class KavitaLocale +public sealed record KavitaLocale { public string FileName { get; set; } // Key public string RenderName { get; set; } diff --git a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs b/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs index c6d2e07cc..c053bd34e 100644 --- a/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs +++ b/API/DTOs/KavitaPlus/Account/AniListUpdateDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.KavitaPlus.Account; -public class AniListUpdateDto +public sealed record AniListUpdateDto { public string Token { get; set; } } diff --git a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs b/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs index 220bd9e7e..340ad0f4c 100644 --- a/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs +++ b/API/DTOs/KavitaPlus/Account/UserTokenInfo.cs @@ -5,7 +5,7 @@ namespace API.DTOs.KavitaPlus.Account; /// /// Represents information around a user's tokens and their status /// -public class UserTokenInfo +public sealed record UserTokenInfo { public int UserId { get; set; } public string Username { get; set; } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs index 547bb63a8..2b7dea8e6 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/ExternalMetadataIdsDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.KavitaPlus.ExternalMetadata; /// /// Used for matching and fetching metadata on a series /// -internal class ExternalMetadataIdsDto +internal sealed record ExternalMetadataIdsDto { public long? MalId { get; set; } public int? AniListId { get; set; } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs index f63fe5a9e..fae674ded 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/MatchSeriesRequestDto.cs @@ -4,14 +4,18 @@ using API.DTOs.Scrobbling; namespace API.DTOs.KavitaPlus.ExternalMetadata; #nullable enable -internal class MatchSeriesRequestDto +/// +/// Represents a request to match some series from Kavita to an external id which K+ uses. +/// +internal sealed record MatchSeriesRequestDto { - public string SeriesName { get; set; } - public ICollection AlternativeNames { get; set; } + public required string SeriesName { get; set; } + public ICollection AlternativeNames { get; set; } = []; public int Year { get; set; } = 0; - public string Query { get; set; } + public string? Query { get; set; } public int? AniListId { get; set; } public long? MalId { get; set; } public string? HardcoverId { get; set; } + public int? CbrId { get; set; } public PlusMediaFormat Format { get; set; } } diff --git a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs index 26411bce7..d0cbb7bd3 100644 --- a/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs +++ b/API/DTOs/KavitaPlus/ExternalMetadata/SeriesDetailPlusApiDto.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.DTOs.SeriesDetail; namespace API.DTOs.KavitaPlus.ExternalMetadata; -internal class SeriesDetailPlusApiDto +internal sealed record SeriesDetailPlusApiDto { public IEnumerable Recommendations { get; set; } public IEnumerable Reviews { get; set; } diff --git a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs index eedbed2ef..dd85dd063 100644 --- a/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/EncryptLicenseDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs.KavitaPlus.License; #nullable enable -public class EncryptLicenseDto +public sealed record EncryptLicenseDto { public required string License { get; set; } public required string InstallId { get; set; } diff --git a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs b/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs index 398556aac..2cd9b5896 100644 --- a/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs +++ b/API/DTOs/KavitaPlus/License/LicenseInfoDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.KavitaPlus.License; -public class LicenseInfoDto +public sealed record LicenseInfoDto { /// /// If cancelled, will represent cancellation date. If not, will represent repayment date diff --git a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs b/API/DTOs/KavitaPlus/License/LicenseValidDto.cs index 56ee6cf73..a7bd476ce 100644 --- a/API/DTOs/KavitaPlus/License/LicenseValidDto.cs +++ b/API/DTOs/KavitaPlus/License/LicenseValidDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.KavitaPlus.License; -public class LicenseValidDto +public sealed record LicenseValidDto { public required string License { get; set; } public required string InstallId { get; set; } diff --git a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs b/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs index 60496ee0e..d0fd9b666 100644 --- a/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/ResetLicenseDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.KavitaPlus.License; -public class ResetLicenseDto +public sealed record ResetLicenseDto { public required string License { get; set; } public required string InstallId { get; set; } diff --git a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs b/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs index 4621810f0..28b47efbe 100644 --- a/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs +++ b/API/DTOs/KavitaPlus/License/UpdateLicenseDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs.KavitaPlus.License; #nullable enable -public class UpdateLicenseDto +public sealed record UpdateLicenseDto { /// /// License Key received from Kavita+ diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs index 60bed32b0..8eb38c98a 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs +++ b/API/DTOs/KavitaPlus/Manage/ManageMatchFilterDto.cs @@ -12,7 +12,7 @@ public enum MatchStateOption DontMatch = 4 } -public class ManageMatchFilterDto +public sealed record ManageMatchFilterDto { public MatchStateOption MatchStateOption { get; set; } = MatchStateOption.All; public string SearchTerm { get; set; } = string.Empty; diff --git a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs b/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs index 14617e7f0..a51e63ee9 100644 --- a/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs +++ b/API/DTOs/KavitaPlus/Manage/ManageMatchSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.KavitaPlus.Manage; -public class ManageMatchSeriesDto +public sealed record ManageMatchSeriesDto { public SeriesDto Series { get; set; } public bool IsMatched { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs index 6b711513c..1dcd8494c 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalChapterDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.KavitaPlus.Metadata; /// /// Information about an individual issue/chapter/book from Kavita+ /// -public class ExternalChapterDto +public sealed record ExternalChapterDto { public string Title { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs index 2ea746214..a3cd378b2 100644 --- a/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/ExternalSeriesDetailDto.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; -using API.DTOs.KavitaPlus.Metadata; +using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.Services.Plus; -namespace API.DTOs.Recommendation; +namespace API.DTOs.KavitaPlus.Metadata; #nullable enable /// /// This is AniListSeries /// -public class ExternalSeriesDetailDto +public sealed record ExternalSeriesDetailDto { public string Name { get; set; } public int? AniListId { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs b/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs index 796cfeb1a..a9debabd1 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/MetadataFieldMappingDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.KavitaPlus.Metadata; -public class MetadataFieldMappingDto +public sealed record MetadataFieldMappingDto { public int Id { get; set; } public MetadataFieldType SourceType { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs index 1dd26a7bc..e9f6614bc 100644 --- a/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs +++ b/API/DTOs/KavitaPlus/Metadata/MetadataSettingsDto.cs @@ -7,7 +7,7 @@ using NotImplementedException = System.NotImplementedException; namespace API.DTOs.KavitaPlus.Metadata; -public class MetadataSettingsDto +public sealed record MetadataSettingsDto { /// /// If writing any sort of metadata from upstream (AniList, Hardcover) source is allowed diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs index bb5a3f20a..2b57548cd 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs +++ b/API/DTOs/KavitaPlus/Metadata/SeriesCharacter.cs @@ -9,7 +9,7 @@ public enum CharacterRole } -public class SeriesCharacter +public sealed record SeriesCharacter { public string Name { get; set; } public required string Description { get; set; } diff --git a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs b/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs index bd42e73a1..0b1f619a2 100644 --- a/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs +++ b/API/DTOs/KavitaPlus/Metadata/SeriesRelationship.cs @@ -5,7 +5,7 @@ using API.Services.Plus; namespace API.DTOs.KavitaPlus.Metadata; -public class ALMediaTitle +public sealed record ALMediaTitle { public string? EnglishTitle { get; set; } public string RomajiTitle { get; set; } @@ -13,7 +13,7 @@ public class ALMediaTitle public string PreferredTitle { get; set; } } -public class SeriesRelationship +public sealed record SeriesRelationship { public int AniListId { get; set; } public int? MalId { get; set; } diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index 18dea9434..8ba687346 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -1,12 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using API.Entities.Enums; namespace API.DTOs; #nullable enable -public class LibraryDto +public sealed record LibraryDto { public int Id { get; init; } public string? Name { get; init; } diff --git a/API/DTOs/MangaFileDto.cs b/API/DTOs/MangaFileDto.cs index 9f2f19a42..23bb37467 100644 --- a/API/DTOs/MangaFileDto.cs +++ b/API/DTOs/MangaFileDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs; #nullable enable -public class MangaFileDto +public sealed record MangaFileDto { public int Id { get; init; } /// diff --git a/API/DTOs/MediaErrors/MediaErrorDto.cs b/API/DTOs/MediaErrors/MediaErrorDto.cs index bfaf57124..b77ee88be 100644 --- a/API/DTOs/MediaErrors/MediaErrorDto.cs +++ b/API/DTOs/MediaErrors/MediaErrorDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.MediaErrors; -public class MediaErrorDto +public sealed record MediaErrorDto { /// /// Format Type (RAR, ZIP, 7Zip, Epub, PDF) diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index 7b750b32f..f5f24b284 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -8,7 +8,7 @@ namespace API.DTOs; /// /// Represents a member of a Kavita server. /// -public class MemberDto +public sealed record MemberDto { public int Id { get; init; } public string? Username { get; init; } diff --git a/API/DTOs/Metadata/AgeRatingDto.cs b/API/DTOs/Metadata/AgeRatingDto.cs index 07523c3fe..bfa835ef5 100644 --- a/API/DTOs/Metadata/AgeRatingDto.cs +++ b/API/DTOs/Metadata/AgeRatingDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Metadata; -public class AgeRatingDto +public sealed record AgeRatingDto { public AgeRating Value { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/ChapterMetadataDto.cs b/API/DTOs/Metadata/ChapterMetadataDto.cs index bbd93d618..c79436e24 100644 --- a/API/DTOs/Metadata/ChapterMetadataDto.cs +++ b/API/DTOs/Metadata/ChapterMetadataDto.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs.Metadata; @@ -9,7 +10,7 @@ namespace API.DTOs.Metadata; /// Exclusively metadata about a given chapter /// [Obsolete("Will not be maintained as of v0.8.1")] -public class ChapterMetadataDto +public sealed record ChapterMetadataDto { public int Id { get; set; } public int ChapterId { get; set; } diff --git a/API/DTOs/Metadata/GenreTagDto.cs b/API/DTOs/Metadata/GenreTagDto.cs index cf05ebbff..4846048d2 100644 --- a/API/DTOs/Metadata/GenreTagDto.cs +++ b/API/DTOs/Metadata/GenreTagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public class GenreTagDto +public sealed record GenreTagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs b/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs index aefd697ba..774581b37 100644 --- a/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs +++ b/API/DTOs/Metadata/Matching/ExternalSeriesMatchDto.cs @@ -1,8 +1,9 @@ +using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Recommendation; namespace API.DTOs.Metadata.Matching; -public class ExternalSeriesMatchDto +public sealed record ExternalSeriesMatchDto { public ExternalSeriesDetailDto Series { get; set; } public float MatchRating { get; set; } diff --git a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs b/API/DTOs/Metadata/Matching/MatchSeriesDto.cs index 1f401e787..bb497b9ab 100644 --- a/API/DTOs/Metadata/Matching/MatchSeriesDto.cs +++ b/API/DTOs/Metadata/Matching/MatchSeriesDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.Metadata.Matching; /// /// Used for matching a series with Kavita+ for metadata and scrobbling /// -public class MatchSeriesDto +public sealed record MatchSeriesDto { /// /// When set, Kavita will stop attempting to match this series and will not perform any scrobbling diff --git a/API/DTOs/Metadata/PublicationStatusDto.cs b/API/DTOs/Metadata/PublicationStatusDto.cs index b8166a6e5..b4f12500a 100644 --- a/API/DTOs/Metadata/PublicationStatusDto.cs +++ b/API/DTOs/Metadata/PublicationStatusDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Metadata; -public class PublicationStatusDto +public sealed record PublicationStatusDto { public PublicationStatus Value { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/Metadata/TagDto.cs b/API/DTOs/Metadata/TagDto.cs index 59e03a279..f8deb6913 100644 --- a/API/DTOs/Metadata/TagDto.cs +++ b/API/DTOs/Metadata/TagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Metadata; -public class TagDto +public sealed record TagDto { public int Id { get; set; } public required string Title { get; set; } diff --git a/API/DTOs/OPDS/Feed.cs b/API/DTOs/OPDS/Feed.cs index 76a740b89..5f4c4b115 100644 --- a/API/DTOs/OPDS/Feed.cs +++ b/API/DTOs/OPDS/Feed.cs @@ -4,11 +4,13 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; +// TODO: OPDS Dtos are internal state, shouldn't be in DTO directory + /// /// /// [XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")] -public class Feed +public sealed record Feed { [XmlElement("updated")] public string Updated { get; init; } = DateTime.UtcNow.ToString("s"); diff --git a/API/DTOs/OPDS/FeedAuthor.cs b/API/DTOs/OPDS/FeedAuthor.cs index 1fd3e6cd2..4196997dd 100644 --- a/API/DTOs/OPDS/FeedAuthor.cs +++ b/API/DTOs/OPDS/FeedAuthor.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public class FeedAuthor +public sealed record FeedAuthor { [XmlElement("name")] public string Name { get; set; } diff --git a/API/DTOs/OPDS/FeedCategory.cs b/API/DTOs/OPDS/FeedCategory.cs index 3129fab60..2352b4af2 100644 --- a/API/DTOs/OPDS/FeedCategory.cs +++ b/API/DTOs/OPDS/FeedCategory.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public class FeedCategory +public sealed record FeedCategory { [XmlAttribute("scheme")] public string Scheme { get; } = "http://www.bisg.org/standards/bisac_subject/index.html"; diff --git a/API/DTOs/OPDS/FeedEntry.cs b/API/DTOs/OPDS/FeedEntry.cs index da8b53b74..838ebd124 100644 --- a/API/DTOs/OPDS/FeedEntry.cs +++ b/API/DTOs/OPDS/FeedEntry.cs @@ -5,7 +5,7 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; #nullable enable -public class FeedEntry +public sealed record FeedEntry { [XmlElement("updated")] public string Updated { get; init; } = DateTime.UtcNow.ToString("s"); diff --git a/API/DTOs/OPDS/FeedEntryContent.cs b/API/DTOs/OPDS/FeedEntryContent.cs index 3e95ce643..4de9b73bd 100644 --- a/API/DTOs/OPDS/FeedEntryContent.cs +++ b/API/DTOs/OPDS/FeedEntryContent.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public class FeedEntryContent +public sealed record FeedEntryContent { [XmlAttribute("type")] public string Type = "text"; diff --git a/API/DTOs/OPDS/FeedLink.cs b/API/DTOs/OPDS/FeedLink.cs index cff3b6736..28c55bbe8 100644 --- a/API/DTOs/OPDS/FeedLink.cs +++ b/API/DTOs/OPDS/FeedLink.cs @@ -3,7 +3,7 @@ using System.Xml.Serialization; namespace API.DTOs.OPDS; -public class FeedLink +public sealed record FeedLink { [XmlIgnore] public bool IsPageStream { get; set; } diff --git a/API/DTOs/OPDS/OpenSearchDescription.cs b/API/DTOs/OPDS/OpenSearchDescription.cs index cc8392a88..eba26572f 100644 --- a/API/DTOs/OPDS/OpenSearchDescription.cs +++ b/API/DTOs/OPDS/OpenSearchDescription.cs @@ -3,7 +3,7 @@ namespace API.DTOs.OPDS; [XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")] -public class OpenSearchDescription +public sealed record OpenSearchDescription { /// /// Contains a brief human-readable title that identifies this search engine. diff --git a/API/DTOs/OPDS/SearchLink.cs b/API/DTOs/OPDS/SearchLink.cs index dba67f3bd..b4698c221 100644 --- a/API/DTOs/OPDS/SearchLink.cs +++ b/API/DTOs/OPDS/SearchLink.cs @@ -2,7 +2,7 @@ namespace API.DTOs.OPDS; -public class SearchLink +public sealed record SearchLink { [XmlAttribute("type")] public string Type { get; set; } = default!; diff --git a/API/DTOs/Person/BrowsePersonDto.cs b/API/DTOs/Person/BrowsePersonDto.cs index 8d6999973..c7d318e79 100644 --- a/API/DTOs/Person/BrowsePersonDto.cs +++ b/API/DTOs/Person/BrowsePersonDto.cs @@ -1,4 +1,6 @@ -namespace API.DTOs; +using API.DTOs.Person; + +namespace API.DTOs; /// /// Used to browse writers and click in to see their series diff --git a/API/DTOs/Person/PersonDto.cs b/API/DTOs/Person/PersonDto.cs index 511317f2a..db152e3b1 100644 --- a/API/DTOs/Person/PersonDto.cs +++ b/API/DTOs/Person/PersonDto.cs @@ -1,6 +1,6 @@ -using System.Runtime.Serialization; +using System.Collections.Generic; -namespace API.DTOs; +namespace API.DTOs.Person; #nullable enable public class PersonDto @@ -13,6 +13,7 @@ public class PersonDto public string? SecondaryColor { get; set; } public string? CoverImage { get; set; } + public List Aliases { get; set; } = []; public string? Description { get; set; } /// diff --git a/API/DTOs/Person/PersonMergeDto.cs b/API/DTOs/Person/PersonMergeDto.cs new file mode 100644 index 000000000..b5dc23375 --- /dev/null +++ b/API/DTOs/Person/PersonMergeDto.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace API.DTOs; + +public sealed record PersonMergeDto +{ + /// + /// The id of the person being merged into + /// + [Required] + public int DestId { get; init; } + /// + /// The id of the person being merged. This person will be removed, and become an alias of + /// + [Required] + public int SrcId { get; init; } +} diff --git a/API/DTOs/Person/UpdatePersonDto.cs b/API/DTOs/Person/UpdatePersonDto.cs index d21fb7350..b43a45e88 100644 --- a/API/DTOs/Person/UpdatePersonDto.cs +++ b/API/DTOs/Person/UpdatePersonDto.cs @@ -1,9 +1,10 @@ -using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace API.DTOs; #nullable enable -public class UpdatePersonDto +public sealed record UpdatePersonDto { [Required] public int Id { get; init; } @@ -11,6 +12,7 @@ public class UpdatePersonDto public bool CoverImageLocked { get; set; } [Required] public string Name {get; set;} + public IList Aliases { get; set; } = []; public string? Description { get; set; } public int? AniListId { get; set; } diff --git a/API/DTOs/Progress/FullProgressDto.cs b/API/DTOs/Progress/FullProgressDto.cs index 7d0b47f60..4f97ab44a 100644 --- a/API/DTOs/Progress/FullProgressDto.cs +++ b/API/DTOs/Progress/FullProgressDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Progress; /// /// A full progress Record from the DB (not all data, only what's needed for API) /// -public class FullProgressDto +public sealed record FullProgressDto { public int Id { get; set; } public int ChapterId { get; set; } diff --git a/API/DTOs/Progress/ProgressDto.cs b/API/DTOs/Progress/ProgressDto.cs index 9fc9010aa..0add848c5 100644 --- a/API/DTOs/Progress/ProgressDto.cs +++ b/API/DTOs/Progress/ProgressDto.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.Progress; #nullable enable -public class ProgressDto +public sealed record ProgressDto { [Required] public int VolumeId { get; set; } diff --git a/API/DTOs/RatingDto.cs b/API/DTOs/RatingDto.cs index 264d2d43c..101aa7ac5 100644 --- a/API/DTOs/RatingDto.cs +++ b/API/DTOs/RatingDto.cs @@ -1,14 +1,18 @@ -using API.Entities.Enums; +using API.Entities; +using API.Entities.Enums; +using API.Entities.Metadata; using API.Services.Plus; namespace API.DTOs; #nullable enable -public class RatingDto +public sealed record RatingDto { + public int AverageScore { get; set; } public int FavoriteCount { get; set; } public ScrobbleProvider Provider { get; set; } + /// public RatingAuthority Authority { get; set; } = RatingAuthority.User; public string? ProviderUrl { get; set; } } diff --git a/API/DTOs/Reader/BookChapterItem.cs b/API/DTOs/Reader/BookChapterItem.cs index dcfb7b904..892e82e27 100644 --- a/API/DTOs/Reader/BookChapterItem.cs +++ b/API/DTOs/Reader/BookChapterItem.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public class BookChapterItem +public sealed record BookChapterItem { /// /// Name of the Chapter diff --git a/API/DTOs/Reader/BookInfoDto.cs b/API/DTOs/Reader/BookInfoDto.cs index c379f71f8..2473cd5dc 100644 --- a/API/DTOs/Reader/BookInfoDto.cs +++ b/API/DTOs/Reader/BookInfoDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public class BookInfoDto : IChapterInfoDto +public sealed record BookInfoDto : IChapterInfoDto { public string BookTitle { get; set; } = default! ; public int SeriesId { get; set; } diff --git a/API/DTOs/Reader/BookmarkDto.cs b/API/DTOs/Reader/BookmarkDto.cs index ef4cf3d6d..da18fc28e 100644 --- a/API/DTOs/Reader/BookmarkDto.cs +++ b/API/DTOs/Reader/BookmarkDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.Reader; #nullable enable -public class BookmarkDto +public sealed record BookmarkDto { public int Id { get; set; } [Required] diff --git a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs b/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs index 7490f837c..51ccf5cc3 100644 --- a/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs +++ b/API/DTOs/Reader/BulkRemoveBookmarkForSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public class BulkRemoveBookmarkForSeriesDto +public sealed record BulkRemoveBookmarkForSeriesDto { public ICollection SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/Reader/ChapterInfoDto.cs b/API/DTOs/Reader/ChapterInfoDto.cs index 4584a5830..4da08a31d 100644 --- a/API/DTOs/Reader/ChapterInfoDto.cs +++ b/API/DTOs/Reader/ChapterInfoDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.Reader; /// /// Information about the Chapter for the Reader to render /// -public class ChapterInfoDto : IChapterInfoDto +public sealed record ChapterInfoDto : IChapterInfoDto { /// /// The Chapter Number diff --git a/API/DTOs/Reader/CreatePersonalToCDto.cs b/API/DTOs/Reader/CreatePersonalToCDto.cs index 3b80ece4a..95272ca58 100644 --- a/API/DTOs/Reader/CreatePersonalToCDto.cs +++ b/API/DTOs/Reader/CreatePersonalToCDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs.Reader; #nullable enable -public class CreatePersonalToCDto +public sealed record CreatePersonalToCDto { public required int ChapterId { get; set; } public required int VolumeId { get; set; } diff --git a/API/DTOs/Reader/FileDimensionDto.cs b/API/DTOs/Reader/FileDimensionDto.cs index baee20dd0..7a7d2978f 100644 --- a/API/DTOs/Reader/FileDimensionDto.cs +++ b/API/DTOs/Reader/FileDimensionDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public class FileDimensionDto +public sealed record FileDimensionDto { public int Width { get; set; } public int Height { get; set; } diff --git a/API/DTOs/Reader/HourEstimateRangeDto.cs b/API/DTOs/Reader/HourEstimateRangeDto.cs index 8c8bd11a9..3facf8e56 100644 --- a/API/DTOs/Reader/HourEstimateRangeDto.cs +++ b/API/DTOs/Reader/HourEstimateRangeDto.cs @@ -3,7 +3,7 @@ /// /// A range of time to read a selection (series, chapter, etc) /// -public record HourEstimateRangeDto +public sealed record HourEstimateRangeDto { /// /// Min hours to read the selection diff --git a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs b/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs index 50187ec81..4c39f7d76 100644 --- a/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs +++ b/API/DTOs/Reader/MarkMultipleSeriesAsReadDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Reader; -public class MarkMultipleSeriesAsReadDto +public sealed record MarkMultipleSeriesAsReadDto { public IReadOnlyList SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/Reader/MarkReadDto.cs b/API/DTOs/Reader/MarkReadDto.cs index 9bf46a6d5..c6f7367c0 100644 --- a/API/DTOs/Reader/MarkReadDto.cs +++ b/API/DTOs/Reader/MarkReadDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public class MarkReadDto +public sealed record MarkReadDto { public int SeriesId { get; init; } } diff --git a/API/DTOs/Reader/MarkVolumeReadDto.cs b/API/DTOs/Reader/MarkVolumeReadDto.cs index 47ffd2649..be95d2e98 100644 --- a/API/DTOs/Reader/MarkVolumeReadDto.cs +++ b/API/DTOs/Reader/MarkVolumeReadDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public class MarkVolumeReadDto +public sealed record MarkVolumeReadDto { public int SeriesId { get; init; } public int VolumeId { get; init; } diff --git a/API/DTOs/Reader/MarkVolumesReadDto.cs b/API/DTOs/Reader/MarkVolumesReadDto.cs index ebe1cd76c..b07bfbc67 100644 --- a/API/DTOs/Reader/MarkVolumesReadDto.cs +++ b/API/DTOs/Reader/MarkVolumesReadDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Reader; /// /// This is used for bulk updating a set of volume and or chapters in one go /// -public class MarkVolumesReadDto +public sealed record MarkVolumesReadDto { public int SeriesId { get; set; } /// diff --git a/API/DTOs/Reader/PersonalToCDto.cs b/API/DTOs/Reader/PersonalToCDto.cs index 144ed561f..c979d9d78 100644 --- a/API/DTOs/Reader/PersonalToCDto.cs +++ b/API/DTOs/Reader/PersonalToCDto.cs @@ -2,7 +2,7 @@ #nullable enable -public class PersonalToCDto +public sealed record PersonalToCDto { public required int ChapterId { get; set; } public required int PageNumber { get; set; } diff --git a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs b/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs index ed6368a4f..ecbb744c8 100644 --- a/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs +++ b/API/DTOs/Reader/RemoveBookmarkForSeriesDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Reader; -public class RemoveBookmarkForSeriesDto +public sealed record RemoveBookmarkForSeriesDto { public int SeriesId { get; init; } } diff --git a/API/DTOs/ReadingLists/CBL/CblBook.cs b/API/DTOs/ReadingLists/CBL/CblBook.cs index 08930e208..d51795b8d 100644 --- a/API/DTOs/ReadingLists/CBL/CblBook.cs +++ b/API/DTOs/ReadingLists/CBL/CblBook.cs @@ -5,7 +5,7 @@ namespace API.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Book")] -public class CblBook +public sealed record CblBook { [XmlAttribute("Series")] public string Series { get; set; } diff --git a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs b/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs index 70a002884..35234923f 100644 --- a/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs +++ b/API/DTOs/ReadingLists/CBL/CblConflictsDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.ReadingLists.CBL; -public class CblConflictQuestion +public sealed record CblConflictQuestion { public string SeriesName { get; set; } public IList LibrariesIds { get; set; } diff --git a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs b/API/DTOs/ReadingLists/CBL/CblImportSummary.cs index 136a31aa8..b9716421e 100644 --- a/API/DTOs/ReadingLists/CBL/CblImportSummary.cs +++ b/API/DTOs/ReadingLists/CBL/CblImportSummary.cs @@ -75,7 +75,7 @@ public enum CblImportReason InvalidFile = 9, } -public class CblBookResult +public sealed record CblBookResult { /// /// Order in the CBL @@ -114,7 +114,7 @@ public class CblBookResult /// /// Represents the summary from the Import of a given CBL /// -public class CblImportSummaryDto +public sealed record CblImportSummaryDto { public string CblName { get; set; } /// diff --git a/API/DTOs/ReadingLists/CBL/CblReadingList.cs b/API/DTOs/ReadingLists/CBL/CblReadingList.cs index 001e6434b..15b349f42 100644 --- a/API/DTOs/ReadingLists/CBL/CblReadingList.cs +++ b/API/DTOs/ReadingLists/CBL/CblReadingList.cs @@ -5,7 +5,7 @@ namespace API.DTOs.ReadingLists.CBL; [XmlRoot(ElementName="Books")] -public class CblBooks +public sealed record CblBooks { [XmlElement(ElementName="Book")] public List Book { get; set; } @@ -13,7 +13,7 @@ public class CblBooks [XmlRoot(ElementName="ReadingList")] -public class CblReadingList +public sealed record CblReadingList { /// /// Name of the Reading List diff --git a/API/DTOs/ReadingLists/CreateReadingListDto.cs b/API/DTOs/ReadingLists/CreateReadingListDto.cs index 783253007..543215722 100644 --- a/API/DTOs/ReadingLists/CreateReadingListDto.cs +++ b/API/DTOs/ReadingLists/CreateReadingListDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public class CreateReadingListDto +public sealed record CreateReadingListDto { public string Title { get; init; } = default!; } diff --git a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs b/API/DTOs/ReadingLists/DeleteReadingListsDto.cs index 8417f8132..8ce92f939 100644 --- a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs +++ b/API/DTOs/ReadingLists/DeleteReadingListsDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace API.DTOs.ReadingLists; -public class DeleteReadingListsDto +public sealed record DeleteReadingListsDto { [Required] public IList ReadingListIds { get; set; } diff --git a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs b/API/DTOs/ReadingLists/PromoteReadingListsDto.cs index f64bbb5ca..8915274de 100644 --- a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs +++ b/API/DTOs/ReadingLists/PromoteReadingListsDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public class PromoteReadingListsDto +public sealed record PromoteReadingListsDto { public IList ReadingListIds { get; init; } public bool Promoted { get; init; } diff --git a/API/DTOs/ReadingLists/ReadingListCast.cs b/API/DTOs/ReadingLists/ReadingListCast.cs index 4532df7d5..855bb12b7 100644 --- a/API/DTOs/ReadingLists/ReadingListCast.cs +++ b/API/DTOs/ReadingLists/ReadingListCast.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using API.DTOs.Person; namespace API.DTOs.ReadingLists; -public class ReadingListCast +public sealed record ReadingListCast { public ICollection Writers { get; set; } = []; public ICollection CoverArtists { get; set; } = []; diff --git a/API/DTOs/ReadingLists/ReadingListDto.cs b/API/DTOs/ReadingLists/ReadingListDto.cs index 6508e7bd4..cbc16275d 100644 --- a/API/DTOs/ReadingLists/ReadingListDto.cs +++ b/API/DTOs/ReadingLists/ReadingListDto.cs @@ -5,7 +5,7 @@ using API.Entities.Interfaces; namespace API.DTOs.ReadingLists; #nullable enable -public class ReadingListDto : IHasCoverImage +public sealed record ReadingListDto : IHasCoverImage { public int Id { get; init; } public string Title { get; set; } = default!; @@ -20,8 +20,8 @@ public class ReadingListDto : IHasCoverImage /// public string? CoverImage { get; set; } = string.Empty; - public string PrimaryColor { get; set; } = string.Empty; - public string SecondaryColor { get; set; } = string.Empty; + public string? PrimaryColor { get; set; } = string.Empty; + public string? SecondaryColor { get; set; } = string.Empty; /// /// Number of Items in the Reading List diff --git a/API/DTOs/ReadingLists/ReadingListInfoDto.cs b/API/DTOs/ReadingLists/ReadingListInfoDto.cs index bd95b9226..64a305f43 100644 --- a/API/DTOs/ReadingLists/ReadingListInfoDto.cs +++ b/API/DTOs/ReadingLists/ReadingListInfoDto.cs @@ -3,7 +3,7 @@ using API.Entities.Interfaces; namespace API.DTOs.ReadingLists; -public class ReadingListInfoDto : IHasReadTimeEstimate +public sealed record ReadingListInfoDto : IHasReadTimeEstimate { /// /// Total Pages across all Reading List Items diff --git a/API/DTOs/ReadingLists/ReadingListItemDto.cs b/API/DTOs/ReadingLists/ReadingListItemDto.cs index 4fca5360c..8edec14f1 100644 --- a/API/DTOs/ReadingLists/ReadingListItemDto.cs +++ b/API/DTOs/ReadingLists/ReadingListItemDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.ReadingLists; #nullable enable -public class ReadingListItemDto +public sealed record ReadingListItemDto { public int Id { get; init; } public int Order { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs index 985f86ac0..6624c8a5c 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByChapterDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public class UpdateReadingListByChapterDto +public sealed record UpdateReadingListByChapterDto { public int ChapterId { get; init; } public int SeriesId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs index 408963529..ba7625088 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByMultipleDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public class UpdateReadingListByMultipleDto +public sealed record UpdateReadingListByMultipleDto { public int SeriesId { get; init; } public int ReadingListId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs index f910e9c06..910a5744d 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByMultipleSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public class UpdateReadingListByMultipleSeriesDto +public sealed record UpdateReadingListByMultipleSeriesDto { public int ReadingListId { get; init; } public IReadOnlyList SeriesIds { get; init; } = default!; diff --git a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs b/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs index 0590882bd..4bb4aa7bb 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListBySeriesDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public class UpdateReadingListBySeriesDto +public sealed record UpdateReadingListBySeriesDto { public int SeriesId { get; init; } public int ReadingListId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs b/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs index f77c7d63a..422d1cc34 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListByVolumeDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.ReadingLists; -public class UpdateReadingListByVolumeDto +public sealed record UpdateReadingListByVolumeDto { public int VolumeId { get; init; } public int SeriesId { get; init; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListDto.cs b/API/DTOs/ReadingLists/UpdateReadingListDto.cs index 6b590707a..de273d825 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListDto.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.ReadingLists; -public class UpdateReadingListDto +public sealed record UpdateReadingListDto { [Required] public int ReadingListId { get; set; } diff --git a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs b/API/DTOs/ReadingLists/UpdateReadingListPosition.cs index 3d0487144..04f2501a8 100644 --- a/API/DTOs/ReadingLists/UpdateReadingListPosition.cs +++ b/API/DTOs/ReadingLists/UpdateReadingListPosition.cs @@ -5,7 +5,7 @@ namespace API.DTOs.ReadingLists; /// /// DTO for moving a reading list item to another position within the same list /// -public class UpdateReadingListPosition +public sealed record UpdateReadingListPosition { [Required] public int ReadingListId { get; set; } [Required] public int ReadingListItemId { get; set; } diff --git a/API/DTOs/Recommendation/ExternalSeriesDto.cs b/API/DTOs/Recommendation/ExternalSeriesDto.cs index d393443af..752001a39 100644 --- a/API/DTOs/Recommendation/ExternalSeriesDto.cs +++ b/API/DTOs/Recommendation/ExternalSeriesDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.Recommendation; #nullable enable -public class ExternalSeriesDto +public sealed record ExternalSeriesDto { public required string Name { get; set; } public required string CoverUrl { get; set; } diff --git a/API/DTOs/Recommendation/MetadataTagDto.cs b/API/DTOs/Recommendation/MetadataTagDto.cs index b219dedc1..a7eb76284 100644 --- a/API/DTOs/Recommendation/MetadataTagDto.cs +++ b/API/DTOs/Recommendation/MetadataTagDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Recommendation; -public class MetadataTagDto +public sealed record MetadataTagDto { public string Name { get; set; } public string Description { get; private set; } diff --git a/API/DTOs/Recommendation/RecommendationDto.cs b/API/DTOs/Recommendation/RecommendationDto.cs index 679245a87..387661324 100644 --- a/API/DTOs/Recommendation/RecommendationDto.cs +++ b/API/DTOs/Recommendation/RecommendationDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Recommendation; -public class RecommendationDto +public sealed record RecommendationDto { public IList OwnedSeries { get; set; } = new List(); public IList ExternalSeries { get; set; } = new List(); diff --git a/API/DTOs/Recommendation/SeriesStaffDto.cs b/API/DTOs/Recommendation/SeriesStaffDto.cs index e4c6f6423..e074e8625 100644 --- a/API/DTOs/Recommendation/SeriesStaffDto.cs +++ b/API/DTOs/Recommendation/SeriesStaffDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs.Recommendation; #nullable enable -public class SeriesStaffDto +public sealed record SeriesStaffDto { public required string Name { get; set; } public string? FirstName { get; set; } diff --git a/API/DTOs/RefreshSeriesDto.cs b/API/DTOs/RefreshSeriesDto.cs index 0e94fc44b..ad26afba2 100644 --- a/API/DTOs/RefreshSeriesDto.cs +++ b/API/DTOs/RefreshSeriesDto.cs @@ -3,7 +3,7 @@ /// /// Used for running some task against a Series. /// -public class RefreshSeriesDto +public sealed record RefreshSeriesDto { /// /// Library Id series belongs to diff --git a/API/DTOs/RegisterDto.cs b/API/DTOs/RegisterDto.cs index 2d4d3b77f..e117af872 100644 --- a/API/DTOs/RegisterDto.cs +++ b/API/DTOs/RegisterDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs; #nullable enable -public class RegisterDto +public sealed record RegisterDto { [Required] public string Username { get; init; } = default!; diff --git a/API/DTOs/ScanFolderDto.cs b/API/DTOs/ScanFolderDto.cs index 684de909e..141f7f0b5 100644 --- a/API/DTOs/ScanFolderDto.cs +++ b/API/DTOs/ScanFolderDto.cs @@ -3,7 +3,7 @@ /// /// DTO for requesting a folder to be scanned /// -public class ScanFolderDto +public sealed record ScanFolderDto { /// /// Api key for a user with Admin permissions diff --git a/API/DTOs/Scrobbling/MalUserInfoDto.cs b/API/DTOs/Scrobbling/MalUserInfoDto.cs index 407639e2a..b6fefc053 100644 --- a/API/DTOs/Scrobbling/MalUserInfoDto.cs +++ b/API/DTOs/Scrobbling/MalUserInfoDto.cs @@ -3,7 +3,7 @@ /// /// Information about a User's MAL connection /// -public class MalUserInfoDto +public sealed record MalUserInfoDto { public required string Username { get; set; } /// diff --git a/API/DTOs/Scrobbling/MediaRecommendationDto.cs b/API/DTOs/Scrobbling/MediaRecommendationDto.cs index 3f565296b..476d77279 100644 --- a/API/DTOs/Scrobbling/MediaRecommendationDto.cs +++ b/API/DTOs/Scrobbling/MediaRecommendationDto.cs @@ -4,7 +4,7 @@ using API.Services.Plus; namespace API.DTOs.Scrobbling; #nullable enable -public record MediaRecommendationDto +public sealed record MediaRecommendationDto { public int Rating { get; set; } public IEnumerable RecommendationNames { get; set; } = null!; diff --git a/API/DTOs/Scrobbling/PlusSeriesDto.cs b/API/DTOs/Scrobbling/PlusSeriesDto.cs index dca9aca92..4d0ef4ea1 100644 --- a/API/DTOs/Scrobbling/PlusSeriesDto.cs +++ b/API/DTOs/Scrobbling/PlusSeriesDto.cs @@ -4,7 +4,7 @@ /// /// Represents information about a potential Series for Kavita+ /// -public record PlusSeriesRequestDto +public sealed record PlusSeriesRequestDto { public int? AniListId { get; set; } public long? MalId { get; set; } diff --git a/API/DTOs/Scrobbling/ScrobbleDto.cs b/API/DTOs/Scrobbling/ScrobbleDto.cs index e8420e785..b90441059 100644 --- a/API/DTOs/Scrobbling/ScrobbleDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleDto.cs @@ -36,7 +36,7 @@ public enum PlusMediaFormat } -public class ScrobbleDto +public sealed record ScrobbleDto { /// /// User's access token to allow us to talk on their behalf diff --git a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs b/API/DTOs/Scrobbling/ScrobbleErrorDto.cs index da85f28f1..7caaad1ca 100644 --- a/API/DTOs/Scrobbling/ScrobbleErrorDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleErrorDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Scrobbling; -public class ScrobbleErrorDto +public sealed record ScrobbleErrorDto { /// /// Developer defined string diff --git a/API/DTOs/Scrobbling/ScrobbleEventDto.cs b/API/DTOs/Scrobbling/ScrobbleEventDto.cs index b62c87866..7b1ccd75a 100644 --- a/API/DTOs/Scrobbling/ScrobbleEventDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleEventDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.Scrobbling; #nullable enable -public class ScrobbleEventDto +public sealed record ScrobbleEventDto { public string SeriesName { get; set; } public int SeriesId { get; set; } diff --git a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs b/API/DTOs/Scrobbling/ScrobbleHoldDto.cs index dcfe7726f..3e09e4799 100644 --- a/API/DTOs/Scrobbling/ScrobbleHoldDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleHoldDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Scrobbling; -public class ScrobbleHoldDto +public sealed record ScrobbleHoldDto { public string SeriesName { get; set; } public int SeriesId { get; set; } diff --git a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs index a63e955d7..53d3a0cc9 100644 --- a/API/DTOs/Scrobbling/ScrobbleResponseDto.cs +++ b/API/DTOs/Scrobbling/ScrobbleResponseDto.cs @@ -4,7 +4,7 @@ /// /// Response from Kavita+ Scrobble API /// -public class ScrobbleResponseDto +public sealed record ScrobbleResponseDto { public bool Successful { get; set; } public string? ErrorMessage { get; set; } diff --git a/API/DTOs/Search/BookmarkSearchResultDto.cs b/API/DTOs/Search/BookmarkSearchResultDto.cs index 5d53add1f..c11d2a2b8 100644 --- a/API/DTOs/Search/BookmarkSearchResultDto.cs +++ b/API/DTOs/Search/BookmarkSearchResultDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Search; -public class BookmarkSearchResultDto +public sealed record BookmarkSearchResultDto { public int LibraryId { get; set; } public int VolumeId { get; set; } diff --git a/API/DTOs/Search/SearchResultDto.cs b/API/DTOs/Search/SearchResultDto.cs index 6fcae3b5d..c497b55dd 100644 --- a/API/DTOs/Search/SearchResultDto.cs +++ b/API/DTOs/Search/SearchResultDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Search; -public class SearchResultDto +public sealed record SearchResultDto { public int SeriesId { get; init; } public string Name { get; init; } = default!; diff --git a/API/DTOs/Search/SearchResultGroupDto.cs b/API/DTOs/Search/SearchResultGroupDto.cs index f7a622664..11c4bdc08 100644 --- a/API/DTOs/Search/SearchResultGroupDto.cs +++ b/API/DTOs/Search/SearchResultGroupDto.cs @@ -2,6 +2,7 @@ using API.DTOs.Collection; using API.DTOs.CollectionTags; using API.DTOs.Metadata; +using API.DTOs.Person; using API.DTOs.Reader; using API.DTOs.ReadingLists; @@ -10,7 +11,7 @@ namespace API.DTOs.Search; /// /// Represents all Search results for a query /// -public class SearchResultGroupDto +public sealed record SearchResultGroupDto { public IEnumerable Libraries { get; set; } = default!; public IEnumerable Series { get; set; } = default!; diff --git a/API/DTOs/SeriesByIdsDto.cs b/API/DTOs/SeriesByIdsDto.cs index 12e13d96f..cb4c52b1e 100644 --- a/API/DTOs/SeriesByIdsDto.cs +++ b/API/DTOs/SeriesByIdsDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs; -public class SeriesByIdsDto +public sealed record SeriesByIdsDto { public int[] SeriesIds { get; init; } = default!; } diff --git a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs index 0f1a8eb4b..1bea81c84 100644 --- a/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs +++ b/API/DTOs/SeriesDetail/NextExpectedChapterDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SeriesDetail; -public class NextExpectedChapterDto +public sealed record NextExpectedChapterDto { public float ChapterNumber { get; set; } public float VolumeNumber { get; set; } diff --git a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs index 29b9eb263..a186dc295 100644 --- a/API/DTOs/SeriesDetail/RelatedSeriesDto.cs +++ b/API/DTOs/SeriesDetail/RelatedSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SeriesDetail; -public class RelatedSeriesDto +public sealed record RelatedSeriesDto { /// /// The parent relationship Series diff --git a/API/DTOs/SeriesDetail/SeriesDetailDto.cs b/API/DTOs/SeriesDetail/SeriesDetailDto.cs index 65d657c67..c4f15552d 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.SeriesDetail; /// This is a special DTO for a UI page in Kavita. This performs sorting and grouping and returns exactly what UI requires for layout. /// This is subject to change, do not rely on this Data model. /// -public class SeriesDetailDto +public sealed record SeriesDetailDto { /// /// Specials for the Series. These will have their title and range cleaned to remove the special marker and prepare diff --git a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs index 76e77ae2c..95f5f39bd 100644 --- a/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs +++ b/API/DTOs/SeriesDetail/SeriesDetailPlusDto.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Recommendation; namespace API.DTOs.SeriesDetail; @@ -8,7 +9,7 @@ namespace API.DTOs.SeriesDetail; /// All the data from Kavita+ for Series Detail /// /// This is what the UI sees, not what the API sends back -public class SeriesDetailPlusDto +public sealed record SeriesDetailPlusDto { public RecommendationDto? Recommendations { get; set; } public IEnumerable Reviews { get; set; } diff --git a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs index f19ad9ca8..a1bb2057e 100644 --- a/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs +++ b/API/DTOs/SeriesDetail/UpdateRelatedSeriesDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SeriesDetail; -public class UpdateRelatedSeriesDto +public sealed record UpdateRelatedSeriesDto { public int SeriesId { get; set; } public IList Adaptations { get; set; } = default!; diff --git a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs index adff04d6c..7af9441c1 100644 --- a/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UpdateUserReviewDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SeriesDetail; #nullable enable -public class UpdateUserReviewDto +public sealed record UpdateUserReviewDto { public int SeriesId { get; set; } public int? ChapterId { get; set; } diff --git a/API/DTOs/SeriesDetail/UserReviewDto.cs b/API/DTOs/SeriesDetail/UserReviewDto.cs index c8340a40a..9e05bbd65 100644 --- a/API/DTOs/SeriesDetail/UserReviewDto.cs +++ b/API/DTOs/SeriesDetail/UserReviewDto.cs @@ -9,7 +9,7 @@ namespace API.DTOs.SeriesDetail; /// Represents a User Review for a given Series /// /// The user does not need to be a Kavita user -public class UserReviewDto +public sealed record UserReviewDto { /// /// A tagline for the review diff --git a/API/DTOs/SeriesDto.cs b/API/DTOs/SeriesDto.cs index 6aa1ecefd..8a49d4c05 100644 --- a/API/DTOs/SeriesDto.cs +++ b/API/DTOs/SeriesDto.cs @@ -5,14 +5,21 @@ using API.Entities.Interfaces; namespace API.DTOs; #nullable enable -public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage +public sealed record SeriesDto : IHasReadTimeEstimate, IHasCoverImage { + /// public int Id { get; init; } + /// public string? Name { get; init; } + /// public string? OriginalName { get; init; } + /// public string? LocalizedName { get; init; } + /// public string? SortName { get; init; } + /// public int Pages { get; init; } + /// public bool CoverImageLocked { get; set; } /// /// Sum of pages read from linked Volumes. Calculated at API-time. @@ -22,9 +29,7 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage /// DateTime representing last time the series was Read. Calculated at API-time. /// public DateTime LatestReadDate { get; set; } - /// - /// DateTime representing last time a chapter was added to the Series - /// + /// public DateTime LastChapterAdded { get; set; } /// /// Rating from logged in user. Calculated at API-time. @@ -35,17 +40,19 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage /// public bool HasUserRated { get; set; } + /// public MangaFormat Format { get; set; } + /// public DateTime Created { get; set; } - public bool NameLocked { get; set; } + /// public bool SortNameLocked { get; set; } + /// public bool LocalizedNameLocked { get; set; } - /// - /// Total number of words for the series. Only applies to epubs. - /// + /// public long WordCount { get; set; } + /// public int LibraryId { get; set; } public string LibraryName { get; set; } = default!; /// @@ -54,33 +61,25 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage public int MaxHoursToRead { get; set; } /// public float AvgHoursToRead { get; set; } - /// - /// The highest level folder for this Series - /// + /// public string FolderPath { get; set; } = default!; - /// - /// Lowest path (that is under library root) that contains all files for the series. - /// - /// must be used before setting + /// public string? LowestFolderPath { get; set; } - /// - /// The last time the folder for this series was scanned - /// + /// public DateTime LastFolderScanned { get; set; } #region KavitaPlus - /// - /// Do not match the series with any external Metadata service. This will automatically opt it out of scrobbling. - /// + /// public bool DontMatch { get; set; } - /// - /// If the series was unable to match, it will be blacklisted until a manual metadata match overrides it - /// + /// public bool IsBlacklisted { get; set; } #endregion + /// public string? CoverImage { get; set; } - public string PrimaryColor { get; set; } = string.Empty; - public string SecondaryColor { get; set; } = string.Empty; + /// + public string? PrimaryColor { get; set; } = string.Empty; + /// + public string? SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/SeriesMetadataDto.cs b/API/DTOs/SeriesMetadataDto.cs index 3f344dff5..fa745148e 100644 --- a/API/DTOs/SeriesMetadataDto.cs +++ b/API/DTOs/SeriesMetadataDto.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using API.DTOs.Metadata; +using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs; -public class SeriesMetadataDto +public sealed record SeriesMetadataDto { public int Id { get; set; } public string Summary { get; set; } = string.Empty; diff --git a/API/DTOs/Settings/SMTPConfigDto.cs b/API/DTOs/Settings/SMTPConfigDto.cs index 07cc58cb8..c14140062 100644 --- a/API/DTOs/Settings/SMTPConfigDto.cs +++ b/API/DTOs/Settings/SMTPConfigDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Settings; -public class SmtpConfigDto +public sealed record SmtpConfigDto { public string SenderAddress { get; set; } = string.Empty; public string SenderDisplayName { get; set; } = string.Empty; diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 78db88d7d..372042250 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -6,7 +6,7 @@ using API.Services; namespace API.DTOs.Settings; #nullable enable -public class ServerSettingDto +public sealed record ServerSettingDto { public string CacheDirectory { get; set; } = default!; diff --git a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs b/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs index 1b081913d..ae1d927a9 100644 --- a/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs +++ b/API/DTOs/SideNav/BulkUpdateSideNavStreamVisibilityDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SideNav; -public class BulkUpdateSideNavStreamVisibilityDto +public sealed record BulkUpdateSideNavStreamVisibilityDto { public required IList Ids { get; set; } public required bool Visibility { get; set; } diff --git a/API/DTOs/SideNav/ExternalSourceDto.cs b/API/DTOs/SideNav/ExternalSourceDto.cs index e9ae03066..382124e8a 100644 --- a/API/DTOs/SideNav/ExternalSourceDto.cs +++ b/API/DTOs/SideNav/ExternalSourceDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.SideNav; -public class ExternalSourceDto +public sealed record ExternalSourceDto { public required int Id { get; set; } = 0; public required string Name { get; set; } diff --git a/API/DTOs/SideNav/SideNavStreamDto.cs b/API/DTOs/SideNav/SideNavStreamDto.cs index fdef82a08..f4c196244 100644 --- a/API/DTOs/SideNav/SideNavStreamDto.cs +++ b/API/DTOs/SideNav/SideNavStreamDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.SideNav; #nullable enable -public class SideNavStreamDto +public sealed record SideNavStreamDto { public int Id { get; set; } public required string Name { get; set; } diff --git a/API/DTOs/Statistics/Count.cs b/API/DTOs/Statistics/Count.cs index 411b44897..1577e682c 100644 --- a/API/DTOs/Statistics/Count.cs +++ b/API/DTOs/Statistics/Count.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Statistics; -public class StatCount : ICount +public sealed record StatCount : ICount { public T Value { get; set; } = default!; public long Count { get; set; } diff --git a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs b/API/DTOs/Statistics/FileExtensionBreakdownDto.cs index 1f122d992..7a248caef 100644 --- a/API/DTOs/Statistics/FileExtensionBreakdownDto.cs +++ b/API/DTOs/Statistics/FileExtensionBreakdownDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.Statistics; #nullable enable -public class FileExtensionDto +public sealed record FileExtensionDto { public string? Extension { get; set; } public MangaFormat Format { get; set; } @@ -12,7 +12,7 @@ public class FileExtensionDto public long TotalFiles { get; set; } } -public class FileExtensionBreakdownDto +public sealed record FileExtensionBreakdownDto { /// /// Total bytes for all files diff --git a/API/DTOs/Statistics/PagesReadOnADayCount.cs b/API/DTOs/Statistics/PagesReadOnADayCount.cs index b1a6bb1ea..fc56d9cc0 100644 --- a/API/DTOs/Statistics/PagesReadOnADayCount.cs +++ b/API/DTOs/Statistics/PagesReadOnADayCount.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Statistics; -public class PagesReadOnADayCount : ICount +public sealed record PagesReadOnADayCount : ICount { /// /// The day of the readings diff --git a/API/DTOs/Statistics/ReadHistoryEvent.cs b/API/DTOs/Statistics/ReadHistoryEvent.cs index 496148789..5d8262aef 100644 --- a/API/DTOs/Statistics/ReadHistoryEvent.cs +++ b/API/DTOs/Statistics/ReadHistoryEvent.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Statistics; /// /// Represents a single User's reading event /// -public class ReadHistoryEvent +public sealed record ReadHistoryEvent { public int UserId { get; set; } public required string? UserName { get; set; } = default!; diff --git a/API/DTOs/Statistics/ServerStatisticsDto.cs b/API/DTOs/Statistics/ServerStatisticsDto.cs index 57fd5abce..3d22d9a56 100644 --- a/API/DTOs/Statistics/ServerStatisticsDto.cs +++ b/API/DTOs/Statistics/ServerStatisticsDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs.Statistics; #nullable enable -public class ServerStatisticsDto +public sealed record ServerStatisticsDto { public long ChapterCount { get; set; } public long VolumeCount { get; set; } diff --git a/API/DTOs/Statistics/TopReadsDto.cs b/API/DTOs/Statistics/TopReadsDto.cs index 806360533..d11594dca 100644 --- a/API/DTOs/Statistics/TopReadsDto.cs +++ b/API/DTOs/Statistics/TopReadsDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs.Statistics; #nullable enable -public class TopReadDto +public sealed record TopReadDto { public int UserId { get; set; } public string? Username { get; set; } = default!; diff --git a/API/DTOs/Statistics/UserReadStatistics.cs b/API/DTOs/Statistics/UserReadStatistics.cs index 5da4b491e..5c6935c6e 100644 --- a/API/DTOs/Statistics/UserReadStatistics.cs +++ b/API/DTOs/Statistics/UserReadStatistics.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace API.DTOs.Statistics; #nullable enable -public class UserReadStatistics +public sealed record UserReadStatistics { /// /// Total number of pages read diff --git a/API/DTOs/Stats/FileExtensionExportDto.cs b/API/DTOs/Stats/FileExtensionExportDto.cs index 6ed554d75..e881960a5 100644 --- a/API/DTOs/Stats/FileExtensionExportDto.cs +++ b/API/DTOs/Stats/FileExtensionExportDto.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Stats; /// /// Excel export for File Extension Report /// -public class FileExtensionExportDto +public sealed record FileExtensionExportDto { [Name("Path")] public string FilePath { get; set; } diff --git a/API/DTOs/Stats/ServerInfoSlimDto.cs b/API/DTOs/Stats/ServerInfoSlimDto.cs index 0b47fa2f3..f1abb2e1d 100644 --- a/API/DTOs/Stats/ServerInfoSlimDto.cs +++ b/API/DTOs/Stats/ServerInfoSlimDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Stats; /// /// This is just for the Server tab on UI /// -public class ServerInfoSlimDto +public sealed record ServerInfoSlimDto { /// /// Unique Id that represents a unique install diff --git a/API/DTOs/Stats/V3/LibraryStatV3.cs b/API/DTOs/Stats/V3/LibraryStatV3.cs index 51af34b58..33ac86d2b 100644 --- a/API/DTOs/Stats/V3/LibraryStatV3.cs +++ b/API/DTOs/Stats/V3/LibraryStatV3.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs.Stats.V3; -public class LibraryStatV3 +public sealed record LibraryStatV3 { public bool IncludeInDashboard { get; set; } public bool IncludeInSearch { get; set; } diff --git a/API/DTOs/Stats/V3/RelationshipStatV3.cs b/API/DTOs/Stats/V3/RelationshipStatV3.cs index e8e1e7440..37b63cb9a 100644 --- a/API/DTOs/Stats/V3/RelationshipStatV3.cs +++ b/API/DTOs/Stats/V3/RelationshipStatV3.cs @@ -5,7 +5,7 @@ namespace API.DTOs.Stats.V3; /// /// KavitaStats - Information about Series Relationships /// -public class RelationshipStatV3 +public sealed record RelationshipStatV3 { public int Count { get; set; } public RelationKind Relationship { get; set; } diff --git a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs index 0bf95403f..8ed3079f5 100644 --- a/API/DTOs/Stats/V3/ServerInfoV3Dto.cs +++ b/API/DTOs/Stats/V3/ServerInfoV3Dto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.Stats.V3; /// /// Represents information about a Kavita Installation for Kavita Stats v3 API /// -public class ServerInfoV3Dto +public sealed record ServerInfoV3Dto { /// /// Unique Id that represents a unique install diff --git a/API/DTOs/Stats/V3/UserStatV3.cs b/API/DTOs/Stats/V3/UserStatV3.cs index 7f4e080ba..450a2e409 100644 --- a/API/DTOs/Stats/V3/UserStatV3.cs +++ b/API/DTOs/Stats/V3/UserStatV3.cs @@ -5,7 +5,7 @@ using API.Entities.Enums.Device; namespace API.DTOs.Stats.V3; -public class UserStatV3 +public sealed record UserStatV3 { public AgeRestriction AgeRestriction { get; set; } /// diff --git a/API/DTOs/System/DirectoryDto.cs b/API/DTOs/System/DirectoryDto.cs index e6e94f4e4..3b1408f7f 100644 --- a/API/DTOs/System/DirectoryDto.cs +++ b/API/DTOs/System/DirectoryDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.System; -public class DirectoryDto +public sealed record DirectoryDto { /// /// Name of the directory diff --git a/API/DTOs/Theme/ColorScapeDto.cs b/API/DTOs/Theme/ColorScapeDto.cs index 066e87d84..2ebd96e2b 100644 --- a/API/DTOs/Theme/ColorScapeDto.cs +++ b/API/DTOs/Theme/ColorScapeDto.cs @@ -4,7 +4,7 @@ /// /// A set of colors for the color scape system in the UI /// -public class ColorScapeDto +public sealed record ColorScapeDto { public string? Primary { get; set; } public string? Secondary { get; set; } diff --git a/API/DTOs/Theme/DownloadableSiteThemeDto.cs b/API/DTOs/Theme/DownloadableSiteThemeDto.cs index dbcedfe61..b27263d92 100644 --- a/API/DTOs/Theme/DownloadableSiteThemeDto.cs +++ b/API/DTOs/Theme/DownloadableSiteThemeDto.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace API.DTOs.Theme; -public class DownloadableSiteThemeDto +public sealed record DownloadableSiteThemeDto { /// /// Theme Name diff --git a/API/DTOs/Theme/SiteThemeDto.cs b/API/DTOs/Theme/SiteThemeDto.cs index eb2a14904..7ae8369e9 100644 --- a/API/DTOs/Theme/SiteThemeDto.cs +++ b/API/DTOs/Theme/SiteThemeDto.cs @@ -7,7 +7,7 @@ namespace API.DTOs.Theme; /// /// Represents a set of css overrides the user can upload to Kavita and will load into webui /// -public class SiteThemeDto +public sealed record SiteThemeDto { public int Id { get; set; } /// diff --git a/API/DTOs/Theme/UpdateDefaultThemeDto.cs b/API/DTOs/Theme/UpdateDefaultThemeDto.cs index 0f2b129f3..aac0858c3 100644 --- a/API/DTOs/Theme/UpdateDefaultThemeDto.cs +++ b/API/DTOs/Theme/UpdateDefaultThemeDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Theme; -public class UpdateDefaultThemeDto +public sealed record UpdateDefaultThemeDto { public int ThemeId { get; set; } } diff --git a/API/DTOs/Update/UpdateNotificationDto.cs b/API/DTOs/Update/UpdateNotificationDto.cs index 2f9550746..b535684f0 100644 --- a/API/DTOs/Update/UpdateNotificationDto.cs +++ b/API/DTOs/Update/UpdateNotificationDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.Update; /// /// Update Notification denoting a new release available for user to update to /// -public class UpdateNotificationDto +public sealed record UpdateNotificationDto { /// /// Current installed Version diff --git a/API/DTOs/UpdateChapterDto.cs b/API/DTOs/UpdateChapterDto.cs index 2ca0a12a9..9ead8adc8 100644 --- a/API/DTOs/UpdateChapterDto.cs +++ b/API/DTOs/UpdateChapterDto.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using API.DTOs.Metadata; +using API.DTOs.Person; using API.Entities.Enums; namespace API.DTOs; -public class UpdateChapterDto +public sealed record UpdateChapterDto { public int Id { get; init; } public string Summary { get; set; } = string.Empty; diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index de02f304d..9bd47fd39 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -4,7 +4,7 @@ using API.Entities.Enums; namespace API.DTOs; -public class UpdateLibraryDto +public sealed record UpdateLibraryDto { [Required] public int Id { get; init; } diff --git a/API/DTOs/UpdateLibraryForUserDto.cs b/API/DTOs/UpdateLibraryForUserDto.cs index c90b697e2..4ce8d0df8 100644 --- a/API/DTOs/UpdateLibraryForUserDto.cs +++ b/API/DTOs/UpdateLibraryForUserDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs; -public class UpdateLibraryForUserDto +public sealed record UpdateLibraryForUserDto { public required string Username { get; init; } public required IEnumerable SelectedLibraries { get; init; } = new List(); diff --git a/API/DTOs/UpdateRBSDto.cs b/API/DTOs/UpdateRBSDto.cs index a7e0c3fc9..fa8bb78f9 100644 --- a/API/DTOs/UpdateRBSDto.cs +++ b/API/DTOs/UpdateRBSDto.cs @@ -3,7 +3,7 @@ namespace API.DTOs; #nullable enable -public class UpdateRbsDto +public sealed record UpdateRbsDto { public required string Username { get; init; } public IList? Roles { get; init; } diff --git a/API/DTOs/UpdateRatingDto.cs b/API/DTOs/UpdateRatingDto.cs index f462fdc2b..472a94fe9 100644 --- a/API/DTOs/UpdateRatingDto.cs +++ b/API/DTOs/UpdateRatingDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs; -public class UpdateRatingDto +public sealed record UpdateRatingDto { public int SeriesId { get; init; } public int? ChapterId { get; init; } diff --git a/API/DTOs/UpdateSeriesDto.cs b/API/DTOs/UpdateSeriesDto.cs index ab4ffcb22..a4a9baf8c 100644 --- a/API/DTOs/UpdateSeriesDto.cs +++ b/API/DTOs/UpdateSeriesDto.cs @@ -1,7 +1,7 @@ namespace API.DTOs; #nullable enable -public class UpdateSeriesDto +public sealed record UpdateSeriesDto { public int Id { get; init; } public string? LocalizedName { get; init; } diff --git a/API/DTOs/UpdateSeriesMetadataDto.cs b/API/DTOs/UpdateSeriesMetadataDto.cs index 75150b3fa..5225f5486 100644 --- a/API/DTOs/UpdateSeriesMetadataDto.cs +++ b/API/DTOs/UpdateSeriesMetadataDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs; -public class UpdateSeriesMetadataDto +public sealed record UpdateSeriesMetadataDto { public SeriesMetadataDto SeriesMetadata { get; set; } = null!; } diff --git a/API/DTOs/Uploads/UploadFileDto.cs b/API/DTOs/Uploads/UploadFileDto.cs index 72fe7da9b..8d5cdf4cb 100644 --- a/API/DTOs/Uploads/UploadFileDto.cs +++ b/API/DTOs/Uploads/UploadFileDto.cs @@ -1,6 +1,6 @@ namespace API.DTOs.Uploads; -public class UploadFileDto +public sealed record UploadFileDto { /// /// Id of the Entity diff --git a/API/DTOs/Uploads/UploadUrlDto.cs b/API/DTOs/Uploads/UploadUrlDto.cs index f2699befd..3f4e625c3 100644 --- a/API/DTOs/Uploads/UploadUrlDto.cs +++ b/API/DTOs/Uploads/UploadUrlDto.cs @@ -2,7 +2,7 @@ namespace API.DTOs.Uploads; -public class UploadUrlDto +public sealed record UploadUrlDto { /// /// External url diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index e89e17df9..88dc97a5d 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -5,7 +5,7 @@ using API.DTOs.Account; namespace API.DTOs; #nullable enable -public class UserDto +public sealed record UserDto { public string Username { get; init; } = null!; public string Email { get; init; } = null!; diff --git a/API/DTOs/UserPreferencesDto.cs b/API/DTOs/UserPreferencesDto.cs index 14987ae77..6645a8f39 100644 --- a/API/DTOs/UserPreferencesDto.cs +++ b/API/DTOs/UserPreferencesDto.cs @@ -7,104 +7,61 @@ using API.Entities.Enums.UserPreferences; namespace API.DTOs; #nullable enable -public class UserPreferencesDto +public sealed record UserPreferencesDto { - /// - /// Manga Reader Option: What direction should the next/prev page buttons go - /// + /// [Required] public ReadingDirection ReadingDirection { get; set; } - /// - /// Manga Reader Option: How should the image be scaled to screen - /// + /// [Required] public ScalingOption ScalingOption { get; set; } - /// - /// Manga Reader Option: Which side of a split image should we show first - /// + /// [Required] public PageSplitOption PageSplitOption { get; set; } - /// - /// Manga Reader Option: How the manga reader should perform paging or reading of the file - /// - /// Webtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging - /// by clicking top/bottom sides of reader. - /// - /// + /// [Required] public ReaderMode ReaderMode { get; set; } - /// - /// Manga Reader Option: How many pages to display in the reader at once - /// + /// [Required] public LayoutMode LayoutMode { get; set; } - /// - /// Manga Reader Option: Emulate a book by applying a shadow effect on the pages - /// + /// [Required] public bool EmulateBook { get; set; } - /// - /// Manga Reader Option: Background color of the reader - /// + /// [Required] public string BackgroundColor { get; set; } = "#000000"; - /// - /// Manga Reader Option: Should swiping trigger pagination - /// + /// [Required] public bool SwipeToPaginate { get; set; } - /// - /// Manga Reader Option: Allow the menu to close after 6 seconds without interaction - /// + /// [Required] public bool AutoCloseMenu { get; set; } - /// - /// Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change - /// + /// [Required] public bool ShowScreenHints { get; set; } = true; - /// - /// Manga Reader Option: Allow Automatic Webtoon detection - /// + /// [Required] public bool AllowAutomaticWebtoonReaderDetection { get; set; } - - /// - /// Book Reader Option: Override extra Margin - /// + /// [Required] public int BookReaderMargin { get; set; } - /// - /// Book Reader Option: Override line-height - /// + /// [Required] public int BookReaderLineSpacing { get; set; } - /// - /// Book Reader Option: Override font size - /// + /// [Required] public int BookReaderFontSize { get; set; } - /// - /// Book Reader Option: Maps to the default Kavita font-family (inherit) or an override - /// + /// [Required] public string BookReaderFontFamily { get; set; } = null!; - - /// - /// Book Reader Option: Allows tapping on side of screens to paginate - /// + /// [Required] public bool BookReaderTapToPaginate { get; set; } - /// - /// Book Reader Option: What direction should the next/prev page buttons go - /// + /// [Required] public ReadingDirection BookReaderReadingDirection { get; set; } - - /// - /// Book Reader Option: What writing style should be used, horizontal or vertical. - /// + /// [Required] public WritingStyle BookReaderWritingStyle { get; set; } @@ -116,79 +73,46 @@ public class UserPreferencesDto public SiteThemeDto? Theme { get; set; } [Required] public string BookReaderThemeName { get; set; } = null!; + /// [Required] public BookPageLayoutMode BookReaderLayoutMode { get; set; } - /// - /// Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this. - /// - /// Defaults to false + /// [Required] public bool BookReaderImmersiveMode { get; set; } = false; - /// - /// Global Site Option: If the UI should layout items as Cards or List items - /// - /// Defaults to Cards + /// [Required] public PageLayoutMode GlobalPageLayoutMode { get; set; } = PageLayoutMode.Cards; - /// - /// UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already - /// - /// Defaults to false + /// [Required] public bool BlurUnreadSummaries { get; set; } = false; - /// - /// UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB. - /// + /// [Required] public bool PromptForDownloadSize { get; set; } = true; - /// - /// UI Site Global Setting: Should Kavita disable CSS transitions - /// + /// [Required] public bool NoTransitions { get; set; } = false; - /// - /// When showing series, only parent series or series with no relationships will be returned - /// + /// [Required] public bool CollapseSeriesRelationships { get; set; } = false; - /// - /// UI Site Global Setting: Should series reviews be shared with all users in the server - /// + /// [Required] public bool ShareReviews { get; set; } = false; - /// - /// UI Site Global Setting: The language locale that should be used for the user - /// + /// [Required] public string Locale { get; set; } - /// - /// PDF Reader: Theme of the Reader - /// + /// [Required] public PdfTheme PdfTheme { get; set; } = PdfTheme.Dark; - /// - /// PDF Reader: Scroll mode of the reader - /// + /// [Required] public PdfScrollMode PdfScrollMode { get; set; } = PdfScrollMode.Vertical; - /// - /// PDF Reader: Layout Mode of the reader - /// - [Required] - public PdfLayoutMode PdfLayoutMode { get; set; } = PdfLayoutMode.Multiple; - /// - /// PDF Reader: Spread Mode of the reader - /// + /// [Required] public PdfSpreadMode PdfSpreadMode { get; set; } = PdfSpreadMode.None; - /// - /// Kavita+: Should this account have Scrobbling enabled for AniList - /// + /// public bool AniListScrobblingEnabled { get; set; } - /// - /// Kavita+: Should this account have Want to Read Sync enabled - /// + /// public bool WantToReadSync { get; set; } } diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 8ef22a93b..fffccea59 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Collections.Generic; using API.Entities; using API.Entities.Interfaces; @@ -8,14 +7,15 @@ using API.Services.Tasks.Scanner.Parser; namespace API.DTOs; -public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage +public sealed record VolumeDto : IHasReadTimeEstimate, IHasCoverImage { + /// public int Id { get; set; } - /// + /// public float MinNumber { get; set; } - /// + /// public float MaxNumber { get; set; } - /// + /// public string Name { get; set; } = default!; /// /// This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14 @@ -24,17 +24,21 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage public int Number { get; set; } public int Pages { get; set; } public int PagesRead { get; set; } + /// public DateTime LastModifiedUtc { get; set; } + /// public DateTime CreatedUtc { get; set; } /// /// When chapter was created in local server time /// /// This is required for Tachiyomi Extension + /// public DateTime Created { get; set; } /// /// When chapter was last modified in local server time /// /// This is required for Tachiyomi Extension + /// public DateTime LastModified { get; set; } public int SeriesId { get; set; } public ICollection Chapters { get; set; } = new List(); @@ -64,10 +68,14 @@ public class VolumeDto : IHasReadTimeEstimate, IHasCoverImage return MinNumber.Is(Parser.SpecialVolumeNumber); } + /// public string CoverImage { get; set; } + /// private bool CoverImageLocked { get; set; } - public string PrimaryColor { get; set; } = string.Empty; - public string SecondaryColor { get; set; } = string.Empty; + /// + public string? PrimaryColor { get; set; } = string.Empty; + /// + public string? SecondaryColor { get; set; } = string.Empty; public void ResetColorScape() { diff --git a/API/DTOs/WantToRead/UpdateWantToReadDto.cs b/API/DTOs/WantToRead/UpdateWantToReadDto.cs index f1b38cea2..a5be26857 100644 --- a/API/DTOs/WantToRead/UpdateWantToReadDto.cs +++ b/API/DTOs/WantToRead/UpdateWantToReadDto.cs @@ -6,7 +6,7 @@ namespace API.DTOs.WantToRead; /// /// A list of Series to pass when working with Want To Read APIs /// -public class UpdateWantToReadDto +public sealed record UpdateWantToReadDto { /// /// List of Series Ids that will be Added/Removed diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index c83ff2fa1..ce35ba7ec 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -49,6 +49,7 @@ public sealed class DataContext : IdentityDbContext ReadingList { get; set; } = null!; public DbSet ReadingListItem { get; set; } = null!; public DbSet Person { get; set; } = null!; + public DbSet PersonAlias { get; set; } = null!; public DbSet Genre { get; set; } = null!; public DbSet Tag { get; set; } = null!; public DbSet SiteTheme { get; set; } = null!; @@ -71,6 +72,7 @@ public sealed class DataContext : IdentityDbContext ExternalSeriesMetadata { get; set; } = null!; public DbSet ExternalRecommendation { get; set; } = null!; public DbSet ManualMigrationHistory { get; set; } = null!; + [Obsolete] public DbSet SeriesBlacklist { get; set; } = null!; public DbSet AppUserCollection { get; set; } = null!; public DbSet ChapterPeople { get; set; } = null!; diff --git a/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs b/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs new file mode 100644 index 000000000..5d76571e1 --- /dev/null +++ b/API/Data/Migrations/20250507221026_PersonAliases.Designer.cs @@ -0,0 +1,3571 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250507221026_PersonAliases")] + partial class PersonAliases + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.4"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("AgeRestriction") + .HasColumnType("INTEGER"); + + b.Property("AgeRestrictionIncludeUnknowns") + .HasColumnType("INTEGER"); + + b.Property("AniListAccessToken") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("ConfirmationToken") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("HasRunScrobbleEventGeneration") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("MalAccessToken") + .HasColumnType("TEXT"); + + b.Property("MalUserName") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventGenerationRan") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Page") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserBookmark"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserChapterRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("MissingSeriesFromSource") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("SourceUrl") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TotalSourceCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserCollection"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(4); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserDashboardStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Host") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserExternalSource"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserOnDeckRemoval"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowAutomaticWebtoonReaderDetection") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AniListScrobblingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AutoCloseMenu") + .HasColumnType("INTEGER"); + + b.Property("BackgroundColor") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("#000000"); + + b.Property("BlurUnreadSummaries") + .HasColumnType("INTEGER"); + + b.Property("BookReaderFontFamily") + .HasColumnType("TEXT"); + + b.Property("BookReaderFontSize") + .HasColumnType("INTEGER"); + + b.Property("BookReaderImmersiveMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLayoutMode") + .HasColumnType("INTEGER"); + + b.Property("BookReaderLineSpacing") + .HasColumnType("INTEGER"); + + b.Property("BookReaderMargin") + .HasColumnType("INTEGER"); + + b.Property("BookReaderReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("BookReaderTapToPaginate") + .HasColumnType("INTEGER"); + + b.Property("BookReaderWritingStyle") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("BookThemeName") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("Dark"); + + b.Property("CollapseSeriesRelationships") + .HasColumnType("INTEGER"); + + b.Property("EmulateBook") + .HasColumnType("INTEGER"); + + b.Property("GlobalPageLayoutMode") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("LayoutMode") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("en"); + + b.Property("NoTransitions") + .HasColumnType("INTEGER"); + + b.Property("PageSplitOption") + .HasColumnType("INTEGER"); + + b.Property("PdfScrollMode") + .HasColumnType("INTEGER"); + + b.Property("PdfSpreadMode") + .HasColumnType("INTEGER"); + + b.Property("PdfTheme") + .HasColumnType("INTEGER"); + + b.Property("PromptForDownloadSize") + .HasColumnType("INTEGER"); + + b.Property("ReaderMode") + .HasColumnType("INTEGER"); + + b.Property("ReadingDirection") + .HasColumnType("INTEGER"); + + b.Property("ScalingOption") + .HasColumnType("INTEGER"); + + b.Property("ShareReviews") + .HasColumnType("INTEGER"); + + b.Property("ShowScreenHints") + .HasColumnType("INTEGER"); + + b.Property("SwipeToPaginate") + .HasColumnType("INTEGER"); + + b.Property("ThemeId") + .HasColumnType("INTEGER"); + + b.Property("WantToReadSync") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.HasKey("Id"); + + b.HasIndex("AppUserId") + .IsUnique(); + + b.HasIndex("ThemeId"); + + b.ToTable("AppUserPreferences"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PagesRead") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserProgresses"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("HasBeenRated") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Review") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserRating"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSourceId") + .HasColumnType("INTEGER"); + + b.Property("IsProvided") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("SmartFilterId") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(5); + + b.Property("Visible") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SmartFilterId"); + + b.HasIndex("Visible"); + + b.ToTable("AppUserSideNavStream"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("AppUserSmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("BookScrollId") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("PageNumber") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("ChapterId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserTableOfContent"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("AppUserWantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AverageExternalRating") + .HasColumnType("REAL"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("ISBNLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("ReleaseDateLocked") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("REAL"); + + b.Property("SortOrderLocked") + .HasColumnType("INTEGER"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TitleNameLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("Chapter"); + }); + + modelBuilder.Entity("API.Entities.CollectionTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Promoted") + .IsUnique(); + + b.ToTable("CollectionTag"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EmailAddress") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("LastUsedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DeliveryStatus") + .HasColumnType("TEXT"); + + b.Property("EmailTemplate") + .HasColumnType("TEXT"); + + b.Property("ErrorMessage") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SendDate") + .HasColumnType("TEXT"); + + b.Property("Sent") + .HasColumnType("INTEGER"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("Sent", "AppUserId", "EmailTemplate", "SendDate"); + + b.ToTable("EmailHistory"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Genre", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Genre"); + }); + + modelBuilder.Entity("API.Entities.History.ManualMigrationHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("RanAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ManualMigrationHistory"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowMetadataMatching") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("AllowScrobbling") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderWatching") + .HasColumnType("INTEGER"); + + b.Property("IncludeInDashboard") + .HasColumnType("INTEGER"); + + b.Property("IncludeInRecommended") + .HasColumnType("INTEGER"); + + b.Property("IncludeInSearch") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastScanned") + .HasColumnType("TEXT"); + + b.Property("ManageCollections") + .HasColumnType("INTEGER"); + + b.Property("ManageReadingLists") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Pattern") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryExcludePattern"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FileTypeGroup") + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("LibraryFileTypeGroup"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bytes") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("LastFileAnalysis") + .HasColumnType("TEXT"); + + b.Property("LastFileAnalysisUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.MediaError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MediaError"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("AverageScore") + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("FavoriteCount") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ProviderUrl") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalRating"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRecommendation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("CoverUrl") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("ExternalRecommendation"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Authority") + .HasColumnType("INTEGER"); + + b.Property("Body") + .HasColumnType("TEXT"); + + b.Property("BodyJustText") + .HasColumnType("TEXT"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("RawBody") + .HasColumnType("TEXT"); + + b.Property("Score") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("SiteUrl") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.ToTable("ExternalReview"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AverageExternalRating") + .HasColumnType("INTEGER"); + + b.Property("CbrId") + .HasColumnType("INTEGER"); + + b.Property("GoogleBooksId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("ValidUntilUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.ToTable("ExternalSeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastChecked") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SeriesBlacklist"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AgeRatingLocked") + .HasColumnType("INTEGER"); + + b.Property("CharacterLocked") + .HasColumnType("INTEGER"); + + b.Property("ColoristLocked") + .HasColumnType("INTEGER"); + + b.Property("CoverArtistLocked") + .HasColumnType("INTEGER"); + + b.Property("EditorLocked") + .HasColumnType("INTEGER"); + + b.Property("GenresLocked") + .HasColumnType("INTEGER"); + + b.Property("ImprintLocked") + .HasColumnType("INTEGER"); + + b.Property("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .HasColumnType("INTEGER"); + + b.Property("LocationLocked") + .HasColumnType("INTEGER"); + + b.Property("MaxCount") + .HasColumnType("INTEGER"); + + b.Property("PencillerLocked") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("PublicationStatusLocked") + .HasColumnType("INTEGER"); + + b.Property("PublisherLocked") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYear") + .HasColumnType("INTEGER"); + + b.Property("ReleaseYearLocked") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("SummaryLocked") + .HasColumnType("INTEGER"); + + b.Property("TagsLocked") + .HasColumnType("INTEGER"); + + b.Property("TeamLocked") + .HasColumnType("INTEGER"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("TranslatorLocked") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WriterLocked") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId") + .IsUnique(); + + b.HasIndex("Id", "SeriesId") + .IsUnique(); + + b.ToTable("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RelationKind") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("TargetSeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.HasIndex("TargetSeriesId"); + + b.ToTable("SeriesRelation"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DestinationType") + .HasColumnType("INTEGER"); + + b.Property("DestinationValue") + .HasColumnType("TEXT"); + + b.Property("ExcludeFromSource") + .HasColumnType("INTEGER"); + + b.Property("MetadataSettingsId") + .HasColumnType("INTEGER"); + + b.Property("SourceType") + .HasColumnType("INTEGER"); + + b.Property("SourceValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MetadataSettingsId"); + + b.ToTable("MetadataFieldMapping"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRatingMappings") + .HasColumnType("TEXT"); + + b.Property("Blacklist") + .HasColumnType("TEXT"); + + b.Property("EnableChapterCoverImage") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterPublisher") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterReleaseDate") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableChapterTitle") + .HasColumnType("INTEGER"); + + b.Property("EnableCoverImage") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("EnableGenres") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalizedName") + .HasColumnType("INTEGER"); + + b.Property("EnablePeople") + .HasColumnType("INTEGER"); + + b.Property("EnablePublicationStatus") + .HasColumnType("INTEGER"); + + b.Property("EnableRelationships") + .HasColumnType("INTEGER"); + + b.Property("EnableStartDate") + .HasColumnType("INTEGER"); + + b.Property("EnableSummary") + .HasColumnType("INTEGER"); + + b.Property("EnableTags") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("FirstLastPeopleNaming") + .HasColumnType("INTEGER"); + + b.Property("Overrides") + .HasColumnType("TEXT"); + + b.PrimitiveCollection("PersonRoles") + .HasColumnType("TEXT"); + + b.Property("Whitelist") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("ChapterPeople"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("Asin") + .HasColumnType("TEXT"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("HardcoverId") + .HasColumnType("TEXT"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.Property("SeriesMetadataId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.Property("KavitaPlusConnection") + .HasColumnType("INTEGER"); + + b.Property("OrderWeight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.HasKey("SeriesMetadataId", "PersonId", "Role"); + + b.HasIndex("PersonId"); + + b.ToTable("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("EndingMonth") + .HasColumnType("INTEGER"); + + b.Property("EndingYear") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("Promoted") + .HasColumnType("INTEGER"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("StartingMonth") + .HasColumnType("INTEGER"); + + b.Property("StartingYear") + .HasColumnType("INTEGER"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("ReadingList"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("ReadingListId") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChapterId"); + + b.HasIndex("ReadingListId"); + + b.HasIndex("SeriesId"); + + b.HasIndex("VolumeId"); + + b.ToTable("ReadingListItem"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId") + .HasColumnType("INTEGER"); + + b.Property("ScrobbleEventId1") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ScrobbleEventId1"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleError"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AniListId") + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("ChapterNumber") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ErrorDetails") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsErrored") + .HasColumnType("INTEGER"); + + b.Property("IsProcessed") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("MalId") + .HasColumnType("INTEGER"); + + b.Property("ProcessDateUtc") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("ReviewBody") + .HasColumnType("TEXT"); + + b.Property("ReviewTitle") + .HasColumnType("TEXT"); + + b.Property("ScrobbleEventType") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("VolumeNumber") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("LibraryId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleEvent"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.HasIndex("SeriesId"); + + b.ToTable("ScrobbleHold"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("DontMatch") + .HasColumnType("INTEGER"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .HasColumnType("INTEGER"); + + b.Property("IsBlacklisted") + .HasColumnType("INTEGER"); + + b.Property("LastChapterAdded") + .HasColumnType("TEXT"); + + b.Property("LastChapterAddedUtc") + .HasColumnType("TEXT"); + + b.Property("LastFolderScanned") + .HasColumnType("TEXT"); + + b.Property("LastFolderScannedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("LocalizedName") + .HasColumnType("TEXT"); + + b.Property("LocalizedNameLocked") + .HasColumnType("INTEGER"); + + b.Property("LowestFolderPath") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedLocalizedName") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.ServerSetting", b => + { + b.Property("Key") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.ToTable("ServerSetting"); + }); + + modelBuilder.Entity("API.Entities.ServerStatistics", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterCount") + .HasColumnType("INTEGER"); + + b.Property("FileCount") + .HasColumnType("INTEGER"); + + b.Property("GenreCount") + .HasColumnType("INTEGER"); + + b.Property("PersonCount") + .HasColumnType("INTEGER"); + + b.Property("SeriesCount") + .HasColumnType("INTEGER"); + + b.Property("TagCount") + .HasColumnType("INTEGER"); + + b.Property("UserCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeCount") + .HasColumnType("INTEGER"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ServerStatistics"); + }); + + modelBuilder.Entity("API.Entities.SiteTheme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("CompatibleVersion") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("GitHubPath") + .HasColumnType("TEXT"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("PreviewUrls") + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ShaHash") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SiteTheme"); + }); + + modelBuilder.Entity("API.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("NormalizedTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedTitle") + .IsUnique(); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("REAL"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LookupName") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MaxNumber") + .HasColumnType("REAL"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinNumber") + .HasColumnType("REAL"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("PrimaryColor") + .HasColumnType("TEXT"); + + b.Property("SecondaryColor") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.Property("CollectionsId") + .HasColumnType("INTEGER"); + + b.Property("ItemsId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionsId", "ItemsId"); + + b.HasIndex("ItemsId"); + + b.ToTable("AppUserCollectionSeries"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "GenresId"); + + b.HasIndex("GenresId"); + + b.ToTable("ChapterGenre"); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.Property("ChaptersId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("ChaptersId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("ChapterTag"); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.Property("CollectionTagsId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionTagsId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("CollectionTagSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.Property("ExternalRatingsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRatingsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRatingExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.Property("ExternalRecommendationsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalRecommendationsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalRecommendationExternalSeriesMetadata"); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.Property("ExternalReviewsId") + .HasColumnType("INTEGER"); + + b.Property("ExternalSeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("ExternalReviewsId", "ExternalSeriesMetadatasId"); + + b.HasIndex("ExternalSeriesMetadatasId"); + + b.ToTable("ExternalReviewExternalSeriesMetadata"); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.Property("GenresId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("GenresId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("GenreSeriesMetadata"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("SeriesMetadatasId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("SeriesMetadataTag"); + }); + + modelBuilder.Entity("API.Entities.AppUserBookmark", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Bookmarks") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserChapterRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ChapterRatings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Ratings") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserCollection", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Collections") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserDashboardStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("DashboardStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserExternalSource", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ExternalSources") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserOnDeckRemoval", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserPreferences", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithOne("UserPreferences") + .HasForeignKey("API.Entities.AppUserPreferences", "AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.SiteTheme", "Theme") + .WithMany() + .HasForeignKey("ThemeId"); + + b.Navigation("AppUser"); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("API.Entities.AppUserProgress", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Progresses") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", null) + .WithMany("UserProgress") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany("Progress") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserRating", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Ratings") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany("Ratings") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.AppUserSideNavStream", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SideNavStreams") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUserSmartFilter", "SmartFilter") + .WithMany() + .HasForeignKey("SmartFilterId"); + + b.Navigation("AppUser"); + + b.Navigation("SmartFilter"); + }); + + modelBuilder.Entity("API.Entities.AppUserSmartFilter", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("SmartFilters") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUserTableOfContent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("TableOfContents") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Chapter"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.AppUserWantToRead", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("WantToRead") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Chapters") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Device", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Devices") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.EmailHistory", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryExcludePattern", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryExcludePatterns") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("LibraryFileTypes") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("Files") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalRating", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalRatings") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalReview", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany("ExternalReviews") + .HasForeignKey("ChapterId"); + }); + + modelBuilder.Entity("API.Entities.Metadata.ExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("ExternalSeriesMetadata") + .HasForeignKey("API.Entities.Metadata.ExternalSeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesBlacklist", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithOne("Metadata") + .HasForeignKey("API.Entities.Metadata.SeriesMetadata", "SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesRelation", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Relations") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "TargetSeries") + .WithMany("RelationOf") + .HasForeignKey("TargetSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + + b.Navigation("TargetSeries"); + }); + + modelBuilder.Entity("API.Entities.MetadataFieldMapping", b => + { + b.HasOne("API.Entities.MetadataMatching.MetadataSettings", "MetadataSettings") + .WithMany("FieldMappings") + .HasForeignKey("MetadataSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MetadataSettings"); + }); + + modelBuilder.Entity("API.Entities.Person.ChapterPeople", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany("People") + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("ChapterPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("SeriesMetadataPeople") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", "SeriesMetadata") + .WithMany("People") + .HasForeignKey("SeriesMetadataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + + b.Navigation("SeriesMetadata"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ReadingLists") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.ReadingListItem", b => + { + b.HasOne("API.Entities.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.ReadingList", "ReadingList") + .WithMany("Items") + .HasForeignKey("ReadingListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Volume", "Volume") + .WithMany() + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + + b.Navigation("ReadingList"); + + b.Navigation("Series"); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleError", b => + { + b.HasOne("API.Entities.Scrobble.ScrobbleEvent", "ScrobbleEvent") + .WithMany() + .HasForeignKey("ScrobbleEventId1"); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ScrobbleEvent"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleEvent", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany() + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", "Library") + .WithMany() + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Library"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Scrobble.ScrobbleHold", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("ScrobbleHolds") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", "Series") + .WithMany() + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserCollectionSeries", b => + { + b.HasOne("API.Entities.AppUserCollection", null) + .WithMany() + .HasForeignKey("CollectionsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Series", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterGenre", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChapterTag", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChaptersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CollectionTagSeriesMetadata", b => + { + b.HasOne("API.Entities.CollectionTag", null) + .WithMany() + .HasForeignKey("CollectionTagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRatingExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRating", null) + .WithMany() + .HasForeignKey("ExternalRatingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalRecommendationExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalRecommendation", null) + .WithMany() + .HasForeignKey("ExternalRecommendationsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ExternalReviewExternalSeriesMetadata", b => + { + b.HasOne("API.Entities.Metadata.ExternalReview", null) + .WithMany() + .HasForeignKey("ExternalReviewsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.ExternalSeriesMetadata", null) + .WithMany() + .HasForeignKey("ExternalSeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GenreSeriesMetadata", b => + { + b.HasOne("API.Entities.Genre", null) + .WithMany() + .HasForeignKey("GenresId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SeriesMetadataTag", b => + { + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Bookmarks"); + + b.Navigation("ChapterRatings"); + + b.Navigation("Collections"); + + b.Navigation("DashboardStreams"); + + b.Navigation("Devices"); + + b.Navigation("ExternalSources"); + + b.Navigation("Progresses"); + + b.Navigation("Ratings"); + + b.Navigation("ReadingLists"); + + b.Navigation("ScrobbleHolds"); + + b.Navigation("SideNavStreams"); + + b.Navigation("SmartFilters"); + + b.Navigation("TableOfContents"); + + b.Navigation("UserPreferences"); + + b.Navigation("UserRoles"); + + b.Navigation("WantToRead"); + }); + + modelBuilder.Entity("API.Entities.Chapter", b => + { + b.Navigation("ExternalRatings"); + + b.Navigation("ExternalReviews"); + + b.Navigation("Files"); + + b.Navigation("People"); + + b.Navigation("Ratings"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Metadata.SeriesMetadata", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("API.Entities.MetadataMatching.MetadataSettings", b => + { + b.Navigation("FieldMappings"); + }); + + modelBuilder.Entity("API.Entities.Person.Person", b => + { + b.Navigation("Aliases"); + + b.Navigation("ChapterPeople"); + + b.Navigation("SeriesMetadataPeople"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("ExternalSeriesMetadata"); + + b.Navigation("Metadata"); + + b.Navigation("Progress"); + + b.Navigation("Ratings"); + + b.Navigation("RelationOf"); + + b.Navigation("Relations"); + + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Chapters"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20250507221026_PersonAliases.cs b/API/Data/Migrations/20250507221026_PersonAliases.cs new file mode 100644 index 000000000..cb046a131 --- /dev/null +++ b/API/Data/Migrations/20250507221026_PersonAliases.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class PersonAliases : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PersonAlias", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Alias = table.Column(type: "TEXT", nullable: true), + NormalizedAlias = table.Column(type: "TEXT", nullable: true), + PersonId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersonAlias", x => x.Id); + table.ForeignKey( + name: "FK_PersonAlias_Person_PersonId", + column: x => x.PersonId, + principalTable: "Person", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PersonAlias_PersonId", + table: "PersonAlias", + column: "PersonId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PersonAlias"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index a66568dcc..bdeb3d7c4 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -1836,6 +1836,28 @@ namespace API.Data.Migrations b.ToTable("Person"); }); + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .HasColumnType("TEXT"); + + b.Property("NormalizedAlias") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("PersonAlias"); + }); + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => { b.Property("SeriesMetadataId") @@ -3082,6 +3104,17 @@ namespace API.Data.Migrations b.Navigation("Person"); }); + modelBuilder.Entity("API.Entities.Person.PersonAlias", b => + { + b.HasOne("API.Entities.Person.Person", "Person") + .WithMany("Aliases") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + modelBuilder.Entity("API.Entities.Person.SeriesMetadataPeople", b => { b.HasOne("API.Entities.Person.Person", "Person") @@ -3496,6 +3529,8 @@ namespace API.Data.Migrations modelBuilder.Entity("API.Entities.Person.Person", b => { + b.Navigation("Aliases"); + b.Navigation("ChapterPeople"); b.Navigation("SeriesMetadataPeople"); diff --git a/API/Data/Repositories/ChapterRepository.cs b/API/Data/Repositories/ChapterRepository.cs index 650b9ac93..27d21df74 100644 --- a/API/Data/Repositories/ChapterRepository.cs +++ b/API/Data/Repositories/ChapterRepository.cs @@ -8,6 +8,7 @@ using API.DTOs.Reader; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; +using API.Entities.Metadata; using API.Extensions; using API.Extensions.QueryExtensions; using AutoMapper; @@ -27,6 +28,7 @@ public enum ChapterIncludes Genres = 16, Tags = 32, ExternalReviews = 1 << 6, + ExternalRatings = 1 << 7 } public interface IChapterRepository @@ -51,8 +53,10 @@ public interface IChapterRepository IEnumerable GetChaptersForSeries(int seriesId); Task> GetAllChaptersForSeries(int seriesId); Task GetAverageUserRating(int chapterId, int userId); - Task> GetExternalChapterReviews(int chapterId); - Task> GetExternalChapterRatings(int chapterId); + Task> GetExternalChapterReviewDtos(int chapterId); + Task> GetExternalChapterReview(int chapterId); + Task> GetExternalChapterRatingDtos(int chapterId); + Task> GetExternalChapterRatings(int chapterId); } public class ChapterRepository : IChapterRepository { @@ -332,7 +336,7 @@ public class ChapterRepository : IChapterRepository return avg.HasValue ? (int) (avg.Value * 20) : 0; } - public async Task> GetExternalChapterReviews(int chapterId) + public async Task> GetExternalChapterReviewDtos(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) @@ -342,7 +346,15 @@ public class ChapterRepository : IChapterRepository .ToListAsync(); } - public async Task> GetExternalChapterRatings(int chapterId) + public async Task> GetExternalChapterReview(int chapterId) + { + return await _context.Chapter + .Where(c => c.Id == chapterId) + .SelectMany(c => c.ExternalReviews) + .ToListAsync(); + } + + public async Task> GetExternalChapterRatingDtos(int chapterId) { return await _context.Chapter .Where(c => c.Id == chapterId) @@ -350,4 +362,12 @@ public class ChapterRepository : IChapterRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } + + public async Task> GetExternalChapterRatings(int chapterId) + { + return await _context.Chapter + .Where(c => c.Id == chapterId) + .SelectMany(c => c.ExternalRatings) + .ToListAsync(); + } } diff --git a/API/Data/Repositories/PersonRepository.cs b/API/Data/Repositories/PersonRepository.cs index db66ecd79..dce3f86ef 100644 --- a/API/Data/Repositories/PersonRepository.cs +++ b/API/Data/Repositories/PersonRepository.cs @@ -1,7 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.DTOs; +using API.DTOs.Person; using API.Entities.Enums; using API.Entities.Person; using API.Extensions; @@ -14,6 +16,17 @@ using Microsoft.EntityFrameworkCore; namespace API.Data.Repositories; #nullable enable +[Flags] +public enum PersonIncludes +{ + None = 1 << 0, + Aliases = 1 << 1, + ChapterPeople = 1 << 2, + SeriesPeople = 1 << 3, + + All = Aliases | ChapterPeople | SeriesPeople, +} + public interface IPersonRepository { void Attach(Person person); @@ -23,24 +36,41 @@ public interface IPersonRepository void Remove(SeriesMetadataPeople person); void Update(Person person); - Task> GetAllPeople(); - Task> GetAllPersonDtosAsync(int userId); - Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role); + Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases); + Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.None); + Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.None); Task RemoveAllPeopleNoLongerAssociated(); - Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null); + Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.None); Task GetCoverImageAsync(int personId); Task GetCoverImageByNameAsync(string name); Task> GetRolesForPersonByName(int personId, int userId); Task> GetAllWritersAndSeriesCount(int userId, UserParams userParams); - Task GetPersonById(int personId); - Task GetPersonDtoByName(string name, int userId); + Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None); + Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases); + /// + /// Returns a person matched on normalized name or alias + /// + /// + /// + /// + Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases); Task IsNameUnique(string name); Task> GetSeriesKnownFor(int personId); Task> GetChaptersForPersonByRole(int personId, int userId, PersonRole role); - Task> GetPeopleByNames(List normalizedNames); - Task GetPersonByAniListId(int aniListId); + /// + /// Returns all people with a matching name, or alias + /// + /// + /// + /// + Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases); + Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases); + + Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases); + + Task AnyAliasExist(string alias); } public class PersonRepository : IPersonRepository @@ -99,7 +129,7 @@ public class PersonRepository : IPersonRepository } - public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null) + public async Task> GetAllPeopleDtosForLibrariesAsync(int userId, List? libraryIds = null, PersonIncludes includes = PersonIncludes.Aliases) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); var userLibs = await _context.Library.GetUserLibraries(userId).ToListAsync(); @@ -113,6 +143,7 @@ public class PersonRepository : IPersonRepository .Where(s => userLibs.Contains(s.LibraryId)) .RestrictAgainstAgeRestriction(ageRating) .SelectMany(s => s.Metadata.People.Select(p => p.Person)) + .Includes(includes) .Distinct() .OrderBy(p => p.Name) .AsNoTracking() @@ -193,27 +224,41 @@ public class PersonRepository : IPersonRepository return await PagedList.CreateAsync(query, userParams.PageNumber, userParams.PageSize); } - public async Task GetPersonById(int personId) + public async Task GetPersonById(int personId, PersonIncludes includes = PersonIncludes.None) { return await _context.Person.Where(p => p.Id == personId) + .Includes(includes) .FirstOrDefaultAsync(); } - public async Task GetPersonDtoByName(string name, int userId) + public async Task GetPersonDtoByName(string name, int userId, PersonIncludes includes = PersonIncludes.Aliases) { var normalized = name.ToNormalized(); var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Person .Where(p => p.NormalizedName == normalized) + .Includes(includes) .RestrictAgainstAgeRestriction(ageRating) .ProjectTo(_mapper.ConfigurationProvider) .FirstOrDefaultAsync(); } + public Task GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases) + { + var normalized = name.ToNormalized(); + return _context.Person + .Includes(includes) + .Where(p => p.NormalizedName == normalized || p.Aliases.Any(pa => pa.NormalizedAlias == normalized)) + .FirstOrDefaultAsync(); + } + public async Task IsNameUnique(string name) { - return !(await _context.Person.AnyAsync(p => p.Name == name)); + // Should this use Normalized to check? + return !(await _context.Person + .Includes(PersonIncludes.Aliases) + .AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name))); } public async Task> GetSeriesKnownFor(int personId) @@ -245,45 +290,69 @@ public class PersonRepository : IPersonRepository .ToListAsync(); } - public async Task> GetPeopleByNames(List normalizedNames) + public async Task> GetPeopleByNames(List normalizedNames, PersonIncludes includes = PersonIncludes.Aliases) { return await _context.Person - .Where(p => normalizedNames.Contains(p.NormalizedName)) + .Includes(includes) + .Where(p => normalizedNames.Contains(p.NormalizedName) || p.Aliases.Any(pa => normalizedNames.Contains(pa.NormalizedAlias))) .OrderBy(p => p.Name) .ToListAsync(); } - public async Task GetPersonByAniListId(int aniListId) + public async Task GetPersonByAniListId(int aniListId, PersonIncludes includes = PersonIncludes.Aliases) { return await _context.Person .Where(p => p.AniListId == aniListId) + .Includes(includes) .FirstOrDefaultAsync(); } - public async Task> GetAllPeople() + public async Task> SearchPeople(string searchQuery, PersonIncludes includes = PersonIncludes.Aliases) + { + searchQuery = searchQuery.ToNormalized(); + + return await _context.Person + .Includes(includes) + .Where(p => EF.Functions.Like(p.Name, $"%{searchQuery}%") + || p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%"))) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); + } + + + public async Task AnyAliasExist(string alias) + { + return await _context.PersonAlias.AnyAsync(pa => pa.NormalizedAlias == alias.ToNormalized()); + } + + + public async Task> GetAllPeople(PersonIncludes includes = PersonIncludes.Aliases) { return await _context.Person + .Includes(includes) .OrderBy(p => p.Name) .ToListAsync(); } - public async Task> GetAllPersonDtosAsync(int userId) + public async Task> GetAllPersonDtosAsync(int userId, PersonIncludes includes = PersonIncludes.Aliases) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Person + .Includes(includes) .OrderBy(p => p.Name) .RestrictAgainstAgeRestriction(ageRating) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); } - public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role) + public async Task> GetAllPersonDtosByRoleAsync(int userId, PersonRole role, PersonIncludes includes = PersonIncludes.Aliases) { var ageRating = await _context.AppUser.GetUserAgeRestriction(userId); return await _context.Person .Where(p => p.SeriesMetadataPeople.Any(smp => smp.Role == role) || p.ChapterPeople.Any(cp => cp.Role == role)) // Filter by role in both series and chapters + .Includes(includes) .OrderBy(p => p.Name) .RestrictAgainstAgeRestriction(ageRating) .ProjectTo(_mapper.ConfigurationProvider) diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs index 6d4a14bd9..6992b2950 100644 --- a/API/Data/Repositories/ReadingListRepository.cs +++ b/API/Data/Repositories/ReadingListRepository.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data.Misc; -using API.DTOs; +using API.DTOs.Person; using API.DTOs.ReadingLists; using API.Entities; using API.Entities.Enums; diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 31ddc22f1..e04c944e3 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -13,7 +13,9 @@ using API.DTOs.CollectionTags; using API.DTOs.Dashboard; using API.DTOs.Filtering; using API.DTOs.Filtering.v2; +using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Metadata; +using API.DTOs.Person; using API.DTOs.ReadingLists; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; @@ -57,6 +59,8 @@ public enum SeriesIncludes ExternalRatings = 128, ExternalRecommendations = 256, ExternalMetadata = 512, + + ExternalData = ExternalMetadata | ExternalReviews | ExternalRatings | ExternalRecommendations, } /// @@ -452,11 +456,18 @@ public class SeriesRepository : ISeriesRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - result.Persons = await _context.SeriesMetadata + // I can't work out how to map people in DB layer + var personIds = await _context.SeriesMetadata .SearchPeople(searchQuery, seriesIds) - .Take(maxRecords) - .OrderBy(t => t.NormalizedName) + .Select(p => p.Id) .Distinct() + .OrderBy(id => id) + .Take(maxRecords) + .ToListAsync(); + + result.Persons = await _context.Person + .Where(p => personIds.Contains(p.Id)) + .OrderBy(p => p.NormalizedName) .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); @@ -472,8 +483,8 @@ public class SeriesRepository : ISeriesRepository .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); - result.Files = new List(); - result.Chapters = new List(); + result.Files = []; + result.Chapters = (List) []; if (includeChapterAndFiles) @@ -563,7 +574,13 @@ public class SeriesRepository : ISeriesRepository if (!fullSeries) return await query.ToListAsync(); - return await query.Include(s => s.Volumes) + return await query + .Include(s => s.Volumes) + .ThenInclude(v => v.Chapters) + .ThenInclude(c => c.ExternalRatings) + .Include(s => s.Volumes) + .ThenInclude(v => v.Chapters) + .ThenInclude(c => c.ExternalReviews) .Include(s => s.Relations) .Include(s => s.Metadata) diff --git a/API/Entities/Metadata/ExternalRating.cs b/API/Entities/Metadata/ExternalRating.cs index 9922c7f80..7fc2b9353 100644 --- a/API/Entities/Metadata/ExternalRating.cs +++ b/API/Entities/Metadata/ExternalRating.cs @@ -11,6 +11,9 @@ public class ExternalRating public int AverageScore { get; set; } public int FavoriteCount { get; set; } public ScrobbleProvider Provider { get; set; } + /// + /// Where this rating comes from: Critic or User + /// public RatingAuthority Authority { get; set; } = RatingAuthority.User; public string? ProviderUrl { get; set; } public int SeriesId { get; set; } diff --git a/API/Entities/Person/Person.cs b/API/Entities/Person/Person.cs index 8eed08f5c..ed57fd6d3 100644 --- a/API/Entities/Person/Person.cs +++ b/API/Entities/Person/Person.cs @@ -8,8 +8,7 @@ public class Person : IHasCoverImage public int Id { get; set; } public required string Name { get; set; } public required string NormalizedName { get; set; } - - //public ICollection Aliases { get; set; } = default!; + public ICollection Aliases { get; set; } = []; public string? CoverImage { get; set; } public bool CoverImageLocked { get; set; } @@ -47,8 +46,8 @@ public class Person : IHasCoverImage //public long MetronId { get; set; } = 0; // Relationships - public ICollection ChapterPeople { get; set; } = new List(); - public ICollection SeriesMetadataPeople { get; set; } = new List(); + public ICollection ChapterPeople { get; set; } = []; + public ICollection SeriesMetadataPeople { get; set; } = []; public void ResetColorScape() diff --git a/API/Entities/Person/PersonAlias.cs b/API/Entities/Person/PersonAlias.cs new file mode 100644 index 000000000..f053f608d --- /dev/null +++ b/API/Entities/Person/PersonAlias.cs @@ -0,0 +1,11 @@ +namespace API.Entities.Person; + +public class PersonAlias +{ + public int Id { get; set; } + public required string Alias { get; set; } + public required string NormalizedAlias { get; set; } + + public int PersonId { get; set; } + public Person Person { get; set; } +} diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index e004fcc25..e95c4f65e 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -53,6 +53,7 @@ public static class ApplicationServiceExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/API/Extensions/ImageExtensions.cs b/API/Extensions/ImageExtensions.cs index 720f572a9..5779b18ec 100644 --- a/API/Extensions/ImageExtensions.cs +++ b/API/Extensions/ImageExtensions.cs @@ -1,18 +1,62 @@ using System; +using System.Collections.Generic; using System.IO; -using NetVips; +using System.Linq; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using Image = NetVips.Image; +using Image = SixLabors.ImageSharp.Image; namespace API.Extensions; public static class ImageExtensions { - public static int GetResolution(this Image image) + + /// + /// Structure to hold various image quality metrics + /// + private sealed class ImageQualityMetrics { - return image.Width * image.Height; + public int Width { get; set; } + public int Height { get; set; } + public bool IsColor { get; set; } + public double Colorfulness { get; set; } + public double Contrast { get; set; } + public double Sharpness { get; set; } + public double NoiseLevel { get; set; } + } + + + /// + /// Calculate a similarity score (0-1f) based on resolution difference and MSE. + /// + /// Path to first image + /// Path to the second image + /// Similarity score between 0-1, where 1 is identical + public static float CalculateSimilarity(this string imagePath1, string imagePath2) + { + if (!File.Exists(imagePath1) || !File.Exists(imagePath2)) + { + throw new FileNotFoundException("One or both image files do not exist"); + } + + // Load both images as Rgba32 (consistent with the rest of the code) + using var img1 = Image.Load(imagePath1); + using var img2 = Image.Load(imagePath2); + + // Calculate resolution difference factor + var res1 = img1.Width * img1.Height; + var res2 = img2.Width * img2.Height; + var resolutionDiff = Math.Abs(res1 - res2) / (float) Math.Max(res1, res2); + + // Calculate mean squared error for pixel differences + var mse = img1.GetMeanSquaredError(img2); + + // Normalize MSE (65025 = 255², which is the max possible squared difference per channel) + var normalizedMse = 1f - Math.Min(1f, mse / 65025f); + + // Final similarity score (weighted average of resolution difference and color difference) + return Math.Max(0f, 1f - (resolutionDiff * 0.5f) - (1f - normalizedMse) * 0.5f); } /// @@ -43,80 +87,351 @@ public static class ImageExtensions } } - return (float)(totalDiff / (img1.Width * img1.Height)); - } - - public static float GetSimilarity(this string imagePath1, string imagePath2) - { - if (!File.Exists(imagePath1) || !File.Exists(imagePath2)) - { - throw new FileNotFoundException("One or both image files do not exist"); - } - - // Calculate similarity score - return CalculateSimilarity(imagePath1, imagePath2); + return (float) (totalDiff / (img1.Width * img1.Height)); } /// - /// Determines which image is "better" based on similarity and resolution. + /// Determines which image is "better" based on multiple quality factors + /// using only the cross-platform ImageSharp library /// /// Path to first image - /// Path to second image - /// Minimum similarity to consider images similar + /// Path to the second image + /// Whether to prefer color images over grayscale (default: true) /// The path of the better image - public static string GetBetterImage(this string imagePath1, string imagePath2, float similarityThreshold = 0.7f) + public static string GetBetterImage(this string imagePath1, string imagePath2, bool preferColor = true) { if (!File.Exists(imagePath1) || !File.Exists(imagePath2)) { throw new FileNotFoundException("One or both image files do not exist"); } - // Calculate similarity score - var similarity = CalculateSimilarity(imagePath1, imagePath2); + // Quick metadata check to get width/height without loading full pixel data + var info1 = Image.Identify(imagePath1); + var info2 = Image.Identify(imagePath2); - using var img1 = Image.NewFromFile(imagePath1, access: Enums.Access.Sequential); - using var img2 = Image.NewFromFile(imagePath2, access: Enums.Access.Sequential); + // Calculate resolution factor + double resolutionFactor1 = info1.Width * info1.Height; + double resolutionFactor2 = info2.Width * info2.Height; - var resolution1 = img1.Width * img1.Height; - var resolution2 = img2.Width * img2.Height; + // If one image is significantly higher resolution (3x or more), just pick it + // This avoids fully loading both images when the choice is obvious + if (resolutionFactor1 > resolutionFactor2 * 3) + return imagePath1; + if (resolutionFactor2 > resolutionFactor1 * 3) + return imagePath2; - // If images are similar, choose the one with higher resolution - if (similarity >= similarityThreshold) + // Otherwise, we need to analyze the actual image data for both + + // NOTE: We HAVE to use these scope blocks and load image here otherwise memory-mapped section exception will occur + ImageQualityMetrics metrics1; + using (var img1 = Image.Load(imagePath1)) { - return resolution1 >= resolution2 ? imagePath1 : imagePath2; + metrics1 = GetImageQualityMetrics(img1); } - // If images are not similar, allow the new image - return imagePath2; + ImageQualityMetrics metrics2; + using (var img2 = Image.Load(imagePath2)) + { + metrics2 = GetImageQualityMetrics(img2); + } + + + // If one is color, and one is grayscale, then we prefer color + if (preferColor && metrics1.IsColor != metrics2.IsColor) + { + return metrics1.IsColor ? imagePath1 : imagePath2; + } + + // Calculate overall quality scores + var score1 = CalculateOverallScore(metrics1); + var score2 = CalculateOverallScore(metrics2); + + return score1 >= score2 ? imagePath1 : imagePath2; + } + + + /// + /// Calculate a weighted overall score based on metrics + /// + private static double CalculateOverallScore(ImageQualityMetrics metrics) + { + // Resolution factor (normalized to HD resolution) + var resolutionFactor = Math.Min(1.0, (metrics.Width * metrics.Height) / (double) (1920 * 1080)); + + // Color factor + var colorFactor = metrics.IsColor ? (0.5 + 0.5 * metrics.Colorfulness) : 0.3; + + // Quality factors + var contrastFactor = Math.Min(1.0, metrics.Contrast); + var sharpnessFactor = Math.Min(1.0, metrics.Sharpness); + + // Noise penalty (less noise is better) + var noisePenalty = Math.Max(0, 1.0 - metrics.NoiseLevel); + + // Weighted combination + return (resolutionFactor * 0.35) + + (colorFactor * 0.3) + + (contrastFactor * 0.15) + + (sharpnessFactor * 0.15) + + (noisePenalty * 0.05); } /// - /// Calculate a similarity score (0-1f) based on resolution difference and MSE. + /// Gets quality metrics for an image /// - /// - /// - /// - private static float CalculateSimilarity(string imagePath1, string imagePath2) + private static ImageQualityMetrics GetImageQualityMetrics(Image image) { - if (!File.Exists(imagePath1) || !File.Exists(imagePath2)) + // Create a smaller version if the image is large to speed up analysis + Image workingImage; + if (image.Width > 512 || image.Height > 512) { - return -1; + workingImage = image.Clone(ctx => ctx.Resize( + new ResizeOptions { + Size = new Size(512), + Mode = ResizeMode.Max + })); + } + else + { + workingImage = image.Clone(); } - using var img1 = Image.NewFromFile(imagePath1, access: Enums.Access.Sequential); - using var img2 = Image.NewFromFile(imagePath2, access: Enums.Access.Sequential); + var metrics = new ImageQualityMetrics + { + Width = image.Width, + Height = image.Height + }; - var res1 = img1.Width * img1.Height; - var res2 = img2.Width * img2.Height; - var resolutionDiff = Math.Abs(res1 - res2) / (float)Math.Max(res1, res2); + // Color analysis (is the image color or grayscale?) + var colorInfo = AnalyzeColorfulness(workingImage); + metrics.IsColor = colorInfo.IsColor; + metrics.Colorfulness = colorInfo.Colorfulness; - using var imgSharp1 = SixLabors.ImageSharp.Image.Load(imagePath1); - using var imgSharp2 = SixLabors.ImageSharp.Image.Load(imagePath2); + // Contrast analysis + metrics.Contrast = CalculateContrast(workingImage); - var mse = imgSharp1.GetMeanSquaredError(imgSharp2); - var normalizedMse = 1f - Math.Min(1f, mse / 65025f); // Normalize based on max color diff + // Sharpness estimation + metrics.Sharpness = EstimateSharpness(workingImage); - // Final similarity score (weighted) - return Math.Max(0f, 1f - (resolutionDiff * 0.5f) - (1f - normalizedMse) * 0.5f); + // Noise estimation + metrics.NoiseLevel = EstimateNoiseLevel(workingImage); + + // Clean up + workingImage.Dispose(); + + return metrics; + } + + /// + /// Analyzes colorfulness of an image + /// + private static (bool IsColor, double Colorfulness) AnalyzeColorfulness(Image image) + { + // For performance, sample a subset of pixels + var sampleSize = Math.Min(1000, image.Width * image.Height); + var stepSize = Math.Max(1, (image.Width * image.Height) / sampleSize); + + var colorCount = 0; + List<(int R, int G, int B)> samples = []; + + // Sample pixels + for (var i = 0; i < image.Width * image.Height; i += stepSize) + { + var x = i % image.Width; + var y = i / image.Width; + + var pixel = image[x, y]; + + // Check if RGB channels differ by a threshold + // High difference indicates color, low difference indicates grayscale + var rMinusG = Math.Abs(pixel.R - pixel.G); + var rMinusB = Math.Abs(pixel.R - pixel.B); + var gMinusB = Math.Abs(pixel.G - pixel.B); + + if (rMinusG > 15 || rMinusB > 15 || gMinusB > 15) + { + colorCount++; + } + + samples.Add((pixel.R, pixel.G, pixel.B)); + } + + // Calculate colorfulness metric based on Hasler and Süsstrunk's approach + // This measures the spread and intensity of colors + if (samples.Count <= 0) return (false, 0); + + // Calculate rg and yb opponent channels + var rg = samples.Select(p => p.R - p.G).ToList(); + var yb = samples.Select(p => 0.5 * (p.R + p.G) - p.B).ToList(); + + // Calculate standard deviation and mean of opponent channels + var rgStdDev = CalculateStdDev(rg); + var ybStdDev = CalculateStdDev(yb); + var rgMean = rg.Average(); + var ybMean = yb.Average(); + + // Combine into colorfulness metric + var stdRoot = Math.Sqrt(rgStdDev * rgStdDev + ybStdDev * ybStdDev); + var meanRoot = Math.Sqrt(rgMean * rgMean + ybMean * ybMean); + + var colorfulness = stdRoot + 0.3 * meanRoot; + + // Normalize to 0-1 range (typical colorfulness is 0-100) + colorfulness = Math.Min(1.0, colorfulness / 100.0); + + var isColor = (double)colorCount / samples.Count > 0.05; + + return (isColor, colorfulness); + + } + + /// + /// Calculate standard deviation of a list of values + /// + private static double CalculateStdDev(List values) + { + var mean = values.Average(); + var sumOfSquaresOfDifferences = values.Select(val => (val - mean) * (val - mean)).Sum(); + return Math.Sqrt(sumOfSquaresOfDifferences / values.Count); + } + + /// + /// Calculate standard deviation of a list of values + /// + private static double CalculateStdDev(List values) + { + var mean = values.Average(); + var sumOfSquaresOfDifferences = values.Select(val => (val - mean) * (val - mean)).Sum(); + return Math.Sqrt(sumOfSquaresOfDifferences / values.Count); + } + + /// + /// Calculates contrast of an image + /// + private static double CalculateContrast(Image image) + { + // For performance, sample a subset of pixels + var sampleSize = Math.Min(1000, image.Width * image.Height); + var stepSize = Math.Max(1, (image.Width * image.Height) / sampleSize); + + List luminanceValues = new(); + + // Sample pixels and calculate luminance + for (var i = 0; i < image.Width * image.Height; i += stepSize) + { + var x = i % image.Width; + var y = i / image.Width; + + var pixel = image[x, y]; + + // Calculate luminance + var luminance = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); + luminanceValues.Add(luminance); + } + + if (luminanceValues.Count < 2) + return 0; + + // Use RMS contrast (root-mean-square of pixel intensity) + var mean = luminanceValues.Average(); + var sumOfSquaresOfDifferences = luminanceValues.Sum(l => Math.Pow(l - mean, 2)); + var rmsContrast = Math.Sqrt(sumOfSquaresOfDifferences / luminanceValues.Count) / mean; + + // Normalize to 0-1 range + return Math.Min(1.0, rmsContrast); + } + + /// + /// Estimates sharpness using simple Laplacian-based method + /// + private static double EstimateSharpness(Image image) + { + // For simplicity, convert to grayscale + var grayImage = new int[image.Width, image.Height]; + + // Convert to grayscale + for (var y = 0; y < image.Height; y++) + { + for (var x = 0; x < image.Width; x++) + { + var pixel = image[x, y]; + grayImage[x, y] = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); + } + } + + // Apply Laplacian filter (3x3) + // The Laplacian measures local variations - higher values indicate edges/details + double laplacianSum = 0; + var validPixels = 0; + + // Laplacian kernel: [0, 1, 0, 1, -4, 1, 0, 1, 0] + for (var y = 1; y < image.Height - 1; y++) + { + for (var x = 1; x < image.Width - 1; x++) + { + var laplacian = + grayImage[x, y - 1] + + grayImage[x - 1, y] - 4 * grayImage[x, y] + grayImage[x + 1, y] + + grayImage[x, y + 1]; + + laplacianSum += Math.Abs(laplacian); + validPixels++; + } + } + + if (validPixels == 0) + return 0; + + // Calculate variance of Laplacian + var laplacianVariance = laplacianSum / validPixels; + + // Normalize to 0-1 range (typical values range from 0-1000) + return Math.Min(1.0, laplacianVariance / 1000.0); + } + + /// + /// Estimates noise level using simple block-based variance method + /// + private static double EstimateNoiseLevel(Image image) + { + // Block size for noise estimation + const int blockSize = 8; + List blockVariances = new(); + + // Calculate variance in small blocks throughout the image + for (var y = 0; y < image.Height - blockSize; y += blockSize) + { + for (var x = 0; x < image.Width - blockSize; x += blockSize) + { + List blockValues = new(); + + // Sample block + for (var by = 0; by < blockSize; by++) + { + for (var bx = 0; bx < blockSize; bx++) + { + var pixel = image[x + bx, y + by]; + var value = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); + blockValues.Add(value); + } + } + + // Calculate variance of this block + var blockMean = blockValues.Average(); + var blockVariance = blockValues.Sum(v => Math.Pow(v - blockMean, 2)) / blockValues.Count; + blockVariances.Add(blockVariance); + } + } + + if (blockVariances.Count == 0) + return 0; + + // Sort block variances and take lowest 10% (likely uniform areas where noise is most visible) + blockVariances.Sort(); + var smoothBlocksCount = Math.Max(1, blockVariances.Count / 10); + var averageNoiseVariance = blockVariances.Take(smoothBlocksCount).Average(); + + // Normalize to 0-1 range (typical noise variances are 0-100) + return Math.Min(1.0, averageNoiseVariance / 100.0); } } diff --git a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs index cc40491d0..d7acf9381 100644 --- a/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs +++ b/API/Extensions/QueryExtensions/Filtering/SearchQueryableExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using API.Data.Misc; using API.Data.Repositories; @@ -49,23 +50,26 @@ public static class SearchQueryableExtensions // Get people from SeriesMetadata var peopleFromSeriesMetadata = queryable .Where(sm => seriesIds.Contains(sm.SeriesId)) - .SelectMany(sm => sm.People) - .Where(p => p.Person.Name != null && EF.Functions.Like(p.Person.Name, $"%{searchQuery}%")) - .Select(p => p.Person); + .SelectMany(sm => sm.People.Select(sp => sp.Person)) + .Where(p => + EF.Functions.Like(p.Name, $"%{searchQuery}%") || + p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")) + ); - // Get people from ChapterPeople by navigating through Volume -> Series var peopleFromChapterPeople = queryable .Where(sm => seriesIds.Contains(sm.SeriesId)) .SelectMany(sm => sm.Series.Volumes) .SelectMany(v => v.Chapters) - .SelectMany(ch => ch.People) - .Where(cp => cp.Person.Name != null && EF.Functions.Like(cp.Person.Name, $"%{searchQuery}%")) - .Select(cp => cp.Person); + .SelectMany(ch => ch.People.Select(cp => cp.Person)) + .Where(p => + EF.Functions.Like(p.Name, $"%{searchQuery}%") || + p.Aliases.Any(pa => EF.Functions.Like(pa.Alias, $"%{searchQuery}%")) + ); // Combine both queries and ensure distinct results return peopleFromSeriesMetadata .Union(peopleFromChapterPeople) - .Distinct() + .Select(p => p) .OrderBy(p => p.NormalizedName); } diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index 1706648c1..bfc585455 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -1,7 +1,7 @@ using System.Linq; using API.Data.Repositories; using API.Entities; -using API.Entities.Metadata; +using API.Entities.Person; using Microsoft.EntityFrameworkCore; namespace API.Extensions.QueryExtensions; @@ -79,6 +79,12 @@ public static class IncludesExtensions .Include(c => c.ExternalReviews); } + if (includes.HasFlag(ChapterIncludes.ExternalRatings)) + { + queryable = queryable + .Include(c => c.ExternalRatings); + } + return queryable.AsSplitQuery(); } @@ -315,4 +321,25 @@ public static class IncludesExtensions return query.AsSplitQuery(); } + + public static IQueryable Includes(this IQueryable queryable, PersonIncludes includeFlags) + { + + if (includeFlags.HasFlag(PersonIncludes.Aliases)) + { + queryable = queryable.Include(p => p.Aliases); + } + + if (includeFlags.HasFlag(PersonIncludes.ChapterPeople)) + { + queryable = queryable.Include(p => p.ChapterPeople); + } + + if (includeFlags.HasFlag(PersonIncludes.SeriesPeople)) + { + queryable = queryable.Include(p => p.SeriesMetadataPeople); + } + + return queryable; + } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 334403ab3..75183fdcd 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -15,6 +15,7 @@ using API.DTOs.KavitaPlus.Manage; using API.DTOs.KavitaPlus.Metadata; using API.DTOs.MediaErrors; using API.DTOs.Metadata; +using API.DTOs.Person; using API.DTOs.Progress; using API.DTOs.Reader; using API.DTOs.ReadingLists; @@ -68,7 +69,8 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName)) .ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count)); - CreateMap(); + CreateMap() + .ForMember(dest => dest.Aliases, opt => opt.MapFrom(src => src.Aliases.Select(s => s.Alias))); CreateMap(); CreateMap(); CreateMap(); diff --git a/API/Helpers/Builders/PersonAliasBuilder.cs b/API/Helpers/Builders/PersonAliasBuilder.cs new file mode 100644 index 000000000..e54ea8975 --- /dev/null +++ b/API/Helpers/Builders/PersonAliasBuilder.cs @@ -0,0 +1,19 @@ +using API.Entities.Person; +using API.Extensions; + +namespace API.Helpers.Builders; + +public class PersonAliasBuilder : IEntityBuilder +{ + private readonly PersonAlias _alias; + public PersonAlias Build() => _alias; + + public PersonAliasBuilder(string name) + { + _alias = new PersonAlias() + { + Alias = name.Trim(), + NormalizedAlias = name.ToNormalized(), + }; + } +} diff --git a/API/Helpers/Builders/PersonBuilder.cs b/API/Helpers/Builders/PersonBuilder.cs index 492d79e17..afd0c84af 100644 --- a/API/Helpers/Builders/PersonBuilder.cs +++ b/API/Helpers/Builders/PersonBuilder.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; -using API.Entities; -using API.Entities.Enums; -using API.Entities.Metadata; +using System.Linq; using API.Entities.Person; using API.Extensions; @@ -34,6 +32,20 @@ public class PersonBuilder : IEntityBuilder return this; } + public PersonBuilder WithAlias(string alias) + { + if (_person.Aliases.Any(a => a.NormalizedAlias.Equals(alias.ToNormalized()))) + { + return this; + } + + _person.Aliases.Add(new PersonAliasBuilder(alias).Build()); + + return this; + } + + + public PersonBuilder WithSeriesMetadata(SeriesMetadataPeople seriesMetadataPeople) { _person.SeriesMetadataPeople.Add(seriesMetadataPeople); diff --git a/API/Helpers/PersonHelper.cs b/API/Helpers/PersonHelper.cs index 07161e418..b71ff2c1a 100644 --- a/API/Helpers/PersonHelper.cs +++ b/API/Helpers/PersonHelper.cs @@ -17,6 +17,20 @@ namespace API.Helpers; public static class PersonHelper { + public static Dictionary ConstructNameAndAliasDictionary(IList people) + { + var dict = new Dictionary(); + foreach (var person in people) + { + dict.TryAdd(person.NormalizedName, person); + foreach (var alias in person.Aliases) + { + dict.TryAdd(alias.NormalizedAlias, person); + } + } + return dict; + } + public static async Task UpdateSeriesMetadataPeopleAsync(SeriesMetadata metadata, ICollection metadataPeople, IEnumerable chapterPeople, PersonRole role, IUnitOfWork unitOfWork) { @@ -38,7 +52,9 @@ public static class PersonHelper // Identify people to remove from metadataPeople var peopleToRemove = existingMetadataPeople - .Where(person => !peopleToAddSet.Contains(person.Person.NormalizedName)) + .Where(person => + !peopleToAddSet.Contains(person.Person.NormalizedName) && + !person.Person.Aliases.Any(pa => peopleToAddSet.Contains(pa.NormalizedAlias))) .ToList(); // Remove identified people from metadataPeople @@ -53,11 +69,7 @@ public static class PersonHelper .GetPeopleByNames(peopleToAdd.Select(p => p.NormalizedName).ToList()); // Prepare a dictionary for quick lookup of existing people by normalized name - var existingPeopleDict = new Dictionary(); - foreach (var person in existingPeopleInDb) - { - existingPeopleDict.TryAdd(person.NormalizedName, person); - } + var existingPeopleDict = ConstructNameAndAliasDictionary(existingPeopleInDb); // Track the people to attach (newly created people) var peopleToAttach = new List(); @@ -129,15 +141,12 @@ public static class PersonHelper var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedPeople); // Prepare a dictionary for quick lookup by normalized name - var existingPeopleDict = new Dictionary(); - foreach (var person in existingPeople) - { - existingPeopleDict.TryAdd(person.NormalizedName, person); - } + var existingPeopleDict = ConstructNameAndAliasDictionary(existingPeople); // Identify people to remove (those present in ChapterPeople but not in the new list) - foreach (var existingChapterPerson in existingChapterPeople - .Where(existingChapterPerson => !normalizedPeople.Contains(existingChapterPerson.Person.NormalizedName))) + var toRemove = existingChapterPeople + .Where(existingChapterPerson => !normalizedPeople.Contains(existingChapterPerson.Person.NormalizedName)); + foreach (var existingChapterPerson in toRemove) { chapter.People.Remove(existingChapterPerson); unitOfWork.PersonRepository.Remove(existingChapterPerson); diff --git a/API/I18N/cs.json b/API/I18N/cs.json index 4b9774218..9825ab074 100644 --- a/API/I18N/cs.json +++ b/API/I18N/cs.json @@ -207,5 +207,6 @@ "dashboard-stream-only-delete-smart-filter": "Z ovládacího panelu lze odstranit pouze streamy chytrých filtrů", "smart-filter-name-required": "Vyžaduje se název chytrého filtru", "smart-filter-system-name": "Nelze použít název streamu poskytovaného systémem", - "sidenav-stream-only-delete-smart-filter": "Z postranní navigace lze odstranit pouze streamy chytrých filtrů" + "sidenav-stream-only-delete-smart-filter": "Z postranní navigace lze odstranit pouze streamy chytrých filtrů", + "aliases-have-overlap": "Jeden nebo více aliasů se překrývají s jinými osobami, nelze je aktualizovat" } diff --git a/API/I18N/en.json b/API/I18N/en.json index 6e37a3cd9..5916bc63e 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -212,6 +212,7 @@ "user-no-access-library-from-series": "User does not have access to the library this series belongs to", "series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions", "kavitaplus-restricted": "This is restricted to Kavita+ only", + "aliases-have-overlap": "One or more of the aliases have overlap with other people, cannot update", "volume-num": "Volume {0}", "book-num": "Book {0}", diff --git a/API/I18N/ga.json b/API/I18N/ga.json index 2d16bcb05..79d0d271e 100644 --- a/API/I18N/ga.json +++ b/API/I18N/ga.json @@ -207,5 +207,6 @@ "smart-filter-system-name": "Ní féidir leat ainm srutha an chórais a sholáthair tú a úsáid", "sidenav-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh as an SideNav", "dashboard-stream-only-delete-smart-filter": "Ní féidir ach sruthanna cliste scagaire a scriosadh ón deais", - "smart-filter-name-required": "Ainm Scagaire Cliste ag teastáil" + "smart-filter-name-required": "Ainm Scagaire Cliste ag teastáil", + "aliases-have-overlap": "Tá forluí idir ceann amháin nó níos mó de na leasainmneacha agus daoine eile, ní féidir iad a nuashonrú" } diff --git a/API/I18N/ko.json b/API/I18N/ko.json index bb087536b..bb49c44ce 100644 --- a/API/I18N/ko.json +++ b/API/I18N/ko.json @@ -203,5 +203,9 @@ "person-name-unique": "개인 이름은 고유해야 합니다", "person-image-doesnt-exist": "CoversDB에 사람이 존재하지 않습니다", "kavitaplus-restricted": "Kavita+만 해당", - "email-taken": "이미 사용중인 이메일" + "email-taken": "이미 사용중인 이메일", + "dashboard-stream-only-delete-smart-filter": "대시보드에서 스마트 필터 스트림만 삭제할 수 있습니다", + "sidenav-stream-only-delete-smart-filter": "사이드 메뉴에서 스마트 필터 스트림만 삭제할 수 있습니다", + "smart-filter-name-required": "스마트 필터 이름이 필요합니다", + "smart-filter-system-name": "시스템 제공 스트림 이름은 사용할 수 없습니다" } diff --git a/API/I18N/pt_BR.json b/API/I18N/pt_BR.json index 7180b3404..b67d926b0 100644 --- a/API/I18N/pt_BR.json +++ b/API/I18N/pt_BR.json @@ -207,5 +207,6 @@ "smart-filter-name-required": "Nome do Filtro Inteligente obrigatório", "dashboard-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do painel", "smart-filter-system-name": "Você não pode usar o nome de um fluxo fornecido pelo sistema", - "sidenav-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do Navegador Lateral" + "sidenav-stream-only-delete-smart-filter": "Somente fluxos de filtros inteligentes podem ser excluídos do Navegador Lateral", + "aliases-have-overlap": "Um ou mais dos pseudônimos se sobrepõem a outras pessoas, não pode atualizar" } diff --git a/API/I18N/ru.json b/API/I18N/ru.json index c75f58fb1..92d842336 100644 --- a/API/I18N/ru.json +++ b/API/I18N/ru.json @@ -121,7 +121,7 @@ "opds-disabled": "OPDS не включен на этом сервере", "stats-permission-denied": "Вы не имеете права просматривать статистику другого пользователя", "reading-list-restricted": "Список чтения не существует или у вас нет доступа", - "favicon-doesnt-exist": "Фавикон не существует", + "favicon-doesnt-exist": "Favicon не существует", "external-source-already-in-use": "Существует поток с этим внешним источником", "issue-num": "Вопрос {0}{1}", "generic-create-temp-archive": "Возникла проблема с созданием временного архива", @@ -194,5 +194,13 @@ "backup": "Резервное копирование", "process-processed-scrobbling-events": "Обработка обработанных событий скроблинга", "scan-libraries": "Сканирование библиотек", - "kavita+-data-refresh": "Обновление данных Kavita+" + "kavita+-data-refresh": "Обновление данных Kavita+", + "kavitaplus-restricted": "Это доступно только для Kavita+", + "person-doesnt-exist": "Персона не существует", + "generic-cover-volume-save": "Не удается сохранить обложку для раздела", + "generic-cover-person-save": "Не удается сохранить изображение обложки для Персоны", + "person-name-unique": "Имя персоны должно быть уникальным", + "person-image-doesnt-exist": "Персона не существует в CoversDB", + "email-taken": "Почта уже используется", + "person-name-required": "Имя персоны обязательно и не может быть пустым" } diff --git a/API/I18N/zh_Hans.json b/API/I18N/zh_Hans.json index 92d1751a8..070a87855 100644 --- a/API/I18N/zh_Hans.json +++ b/API/I18N/zh_Hans.json @@ -207,5 +207,6 @@ "dashboard-stream-only-delete-smart-filter": "只能从仪表板中删除智能筛选器流", "smart-filter-name-required": "需要智能筛选器名称", "smart-filter-system-name": "您不能使用系统提供的流名称", - "sidenav-stream-only-delete-smart-filter": "只能从侧边栏删除智能筛选器流" + "sidenav-stream-only-delete-smart-filter": "只能从侧边栏删除智能筛选器流", + "aliases-have-overlap": "一个或多个别名与其他人有重叠,无法更新" } diff --git a/API/Middleware/SecurityMiddleware.cs b/API/Middleware/SecurityMiddleware.cs index 61ca1c75d..67cb42d0c 100644 --- a/API/Middleware/SecurityMiddleware.cs +++ b/API/Middleware/SecurityMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Text.Json; using System.Threading.Tasks; @@ -26,7 +27,7 @@ public class SecurityEventMiddleware(RequestDelegate next) } catch (KavitaUnauthenticatedUserException ex) { - var ipAddress = context.Connection.RemoteIpAddress?.ToString(); + var ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? context.Connection.RemoteIpAddress?.ToString(); var requestMethod = context.Request.Method; var requestPath = context.Request.Path; var userAgent = context.Request.Headers.UserAgent; diff --git a/API/Program.cs b/API/Program.cs index 77fac9e49..011a7de2a 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; @@ -20,9 +21,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NetVips; using Serilog; using Serilog.Events; using Serilog.Sinks.AspNetCore.SignalR.Extensions; +using Log = Serilog.Log; namespace API; #nullable enable @@ -46,15 +49,13 @@ public class Program var directoryService = new DirectoryService(null!, new FileSystem()); + + // Check if this is the first time running and if so, rename appsettings-init.json to appsettings.json + HandleFirstRunConfiguration(); + + // Before anything, check if JWT has been generated properly or if user still has default - if (!Configuration.CheckIfJwtTokenSet() && - Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) - { - Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions..."); - var rBytes = new byte[256]; - RandomNumberGenerator.Create().GetBytes(rBytes); - Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); - } + EnsureJwtTokenKey(); try { @@ -68,6 +69,7 @@ public class Program { var logger = services.GetRequiredService>(); var context = services.GetRequiredService(); + var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); var isDbCreated = await context.Database.CanConnectAsync(); if (isDbCreated && pendingMigrations.Any()) @@ -143,6 +145,8 @@ public class Program var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); LogLevelOptions.SwitchLogLevel(settings.LoggingLevel); + InitNetVips(); + await host.RunAsync(); } catch (Exception ex) { @@ -153,6 +157,26 @@ public class Program } } + private static void EnsureJwtTokenKey() + { + if (Configuration.CheckIfJwtTokenSet() || Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development) return; + + Log.Logger.Information("Generating JWT TokenKey for encrypting user sessions..."); + var rBytes = new byte[256]; + RandomNumberGenerator.Create().GetBytes(rBytes); + Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty); + } + + private static void HandleFirstRunConfiguration() + { + var firstRunConfigFilePath = Path.Join(Directory.GetCurrentDirectory(), "config/appsettings-init.json"); + if (File.Exists(firstRunConfigFilePath) && + !File.Exists(Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json"))) + { + File.Move(firstRunConfigFilePath, Path.Join(Directory.GetCurrentDirectory(), "config/appsettings.json")); + } + } + private static async Task GetMigrationDirectory(DataContext context, IDirectoryService directoryService) { string? currentVersion = null; @@ -225,4 +249,14 @@ public class Program webBuilder.UseStartup(); }); + + /// + /// Ensure NetVips does not cache + /// + /// https://github.com/kleisauke/net-vips/issues/6#issuecomment-394379299 + private static void InitNetVips() + { + Cache.MaxFiles = 0; + + } } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index ae9383c7b..7e308d92e 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -69,6 +69,7 @@ public interface IDirectoryService IEnumerable GetFiles(string path, string fileNameRegex = "", SearchOption searchOption = SearchOption.TopDirectoryOnly); bool ExistOrCreate(string directoryPath); void DeleteFiles(IEnumerable files); + void CopyFile(string sourcePath, string destinationPath, bool overwrite = true); void RemoveNonImages(string directoryName); void Flatten(string directoryName); Task CheckWriteAccess(string directoryName); @@ -937,6 +938,27 @@ public class DirectoryService : IDirectoryService } } + public void CopyFile(string sourcePath, string destinationPath, bool overwrite = true) + { + if (!File.Exists(sourcePath)) + { + throw new FileNotFoundException("Source file not found", sourcePath); + } + + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (string.IsNullOrEmpty(destinationDirectory)) + { + throw new ArgumentException("Destination path does not contain a directory", nameof(destinationPath)); + } + + if (!Directory.Exists(destinationDirectory)) + { + FileSystem.Directory.CreateDirectory(destinationDirectory); + } + + FileSystem.File.Copy(sourcePath, destinationPath, overwrite); + } + /// /// Returns the human-readable file size for an arbitrary, 64-bit file size /// The default format is "0.## XB", e.g. "4.2 KB" or "1.43 GB" @@ -1090,4 +1112,23 @@ public class DirectoryService : IDirectoryService FlattenDirectory(root, subDirectory, ref directoryIndex); } } + + /// + /// If the file is locked or not existing + /// + /// + /// + public static bool IsFileLocked(string filePath) + { + try + { + if (!File.Exists(filePath)) return false; + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None); + return false; // If this works, the file is not locked + } + catch (IOException) + { + return true; // File is locked by another process + } + } } diff --git a/API/Services/PersonService.cs b/API/Services/PersonService.cs new file mode 100644 index 000000000..ff0049cbe --- /dev/null +++ b/API/Services/PersonService.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.Entities.Person; +using API.Extensions; +using API.Helpers.Builders; + +namespace API.Services; + +public interface IPersonService +{ + /// + /// Adds src as an alias to dst, this is a destructive operation + /// + /// Merged person + /// Remaining person + /// The entities passed as arguments **must** include all relations + /// + Task MergePeopleAsync(Person src, Person dst); + + /// + /// Adds the alias to the person, requires that the aliases are not shared with anyone else + /// + /// This method does NOT commit changes + /// + /// + /// + Task UpdatePersonAliasesAsync(Person person, IList aliases); +} + +public class PersonService(IUnitOfWork unitOfWork): IPersonService +{ + + public async Task MergePeopleAsync(Person src, Person dst) + { + if (dst.Id == src.Id) return; + + if (string.IsNullOrWhiteSpace(dst.Description) && !string.IsNullOrWhiteSpace(src.Description)) + { + dst.Description = src.Description; + } + + if (dst.MalId == 0 && src.MalId != 0) + { + dst.MalId = src.MalId; + } + + if (dst.AniListId == 0 && src.AniListId != 0) + { + dst.AniListId = src.AniListId; + } + + if (dst.HardcoverId == null && src.HardcoverId != null) + { + dst.HardcoverId = src.HardcoverId; + } + + if (dst.Asin == null && src.Asin != null) + { + dst.Asin = src.Asin; + } + + if (dst.CoverImage == null && src.CoverImage != null) + { + dst.CoverImage = src.CoverImage; + } + + MergeChapterPeople(dst, src); + MergeSeriesMetadataPeople(dst, src); + + dst.Aliases.Add(new PersonAliasBuilder(src.Name).Build()); + + foreach (var alias in src.Aliases) + { + dst.Aliases.Add(alias); + } + + unitOfWork.PersonRepository.Remove(src); + unitOfWork.PersonRepository.Update(dst); + await unitOfWork.CommitAsync(); + } + + private static void MergeChapterPeople(Person dst, Person src) + { + + foreach (var chapter in src.ChapterPeople) + { + var alreadyPresent = dst.ChapterPeople + .Any(x => x.ChapterId == chapter.ChapterId && x.Role == chapter.Role); + + if (alreadyPresent) continue; + + dst.ChapterPeople.Add(new ChapterPeople + { + Role = chapter.Role, + ChapterId = chapter.ChapterId, + Person = dst, + KavitaPlusConnection = chapter.KavitaPlusConnection, + OrderWeight = chapter.OrderWeight, + }); + } + } + + private static void MergeSeriesMetadataPeople(Person dst, Person src) + { + foreach (var series in src.SeriesMetadataPeople) + { + var alreadyPresent = dst.SeriesMetadataPeople + .Any(x => x.SeriesMetadataId == series.SeriesMetadataId && x.Role == series.Role); + + if (alreadyPresent) continue; + + dst.SeriesMetadataPeople.Add(new SeriesMetadataPeople + { + SeriesMetadataId = series.SeriesMetadataId, + Role = series.Role, + Person = dst, + KavitaPlusConnection = series.KavitaPlusConnection, + OrderWeight = series.OrderWeight, + }); + } + } + + public async Task UpdatePersonAliasesAsync(Person person, IList aliases) + { + var normalizedAliases = aliases + .Select(a => a.ToNormalized()) + .Where(a => !string.IsNullOrEmpty(a) && a != person.NormalizedName) + .ToList(); + + if (normalizedAliases.Count == 0) + { + person.Aliases = []; + return true; + } + + var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases); + others = others.Where(p => p.Id != person.Id).ToList(); + + if (others.Count != 0) return false; + + person.Aliases = aliases.Select(a => new PersonAliasBuilder(a).Build()).ToList(); + + return true; + } +} diff --git a/API/Services/Plus/ExternalMetadataService.cs b/API/Services/Plus/ExternalMetadataService.cs index c5d0c1f4c..a1e3750dd 100644 --- a/API/Services/Plus/ExternalMetadataService.cs +++ b/API/Services/Plus/ExternalMetadataService.cs @@ -10,6 +10,7 @@ using API.DTOs.Collection; using API.DTOs.KavitaPlus.ExternalMetadata; using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Metadata.Matching; +using API.DTOs.Person; using API.DTOs.Recommendation; using API.DTOs.Scrobbling; using API.DTOs.SeriesDetail; @@ -17,8 +18,10 @@ using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Entities.MetadataMatching; +using API.Entities.Person; using API.Extensions; using API.Helpers; +using API.Helpers.Builders; using API.Services.Tasks.Metadata; using API.Services.Tasks.Scanner.Parser; using API.SignalR; @@ -223,7 +226,7 @@ public class ExternalMetadataService : IExternalMetadataService AlternativeNames = altNames.Where(s => !string.IsNullOrEmpty(s)).ToList(), Year = series.Metadata.ReleaseYear, AniListId = potentialAnilistId ?? ScrobblingService.GetAniListId(series), - MalId = potentialMalId ?? ScrobblingService.GetMalId(series), + MalId = potentialMalId ?? ScrobblingService.GetMalId(series) }; var token = (await _unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; @@ -614,12 +617,8 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = await UpdateTags(series, settings, externalMetadata, processedTags) || madeModification; madeModification = UpdateAgeRating(series, settings, processedGenres.Concat(processedTags)) || madeModification; - var staff = (externalMetadata.Staff ?? []).Select(s => - { - s.Name = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}"; + var staff = await SetNameAndAddAliases(settings, externalMetadata.Staff); - return s; - }).ToList(); madeModification = await UpdateWriters(series, settings, staff) || madeModification; madeModification = await UpdateArtists(series, settings, staff) || madeModification; madeModification = await UpdateCharacters(series, settings, externalMetadata.Characters) || madeModification; @@ -632,6 +631,49 @@ public class ExternalMetadataService : IExternalMetadataService return madeModification; } + private async Task> SetNameAndAddAliases(MetadataSettingsDto settings, IList? staff) + { + if (staff == null || staff.Count == 0) return []; + + var nameMappings = staff.Select(s => new + { + Staff = s, + PreferredName = settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}", + AlternativeName = !settings.FirstLastPeopleNaming ? $"{s.FirstName} {s.LastName}" : $"{s.LastName} {s.FirstName}" + }).ToList(); + + var preferredNames = nameMappings.Select(n => n.PreferredName.ToNormalized()).Distinct().ToList(); + var alternativeNames = nameMappings.Select(n => n.AlternativeName.ToNormalized()).Distinct().ToList(); + + var existingPeople = await _unitOfWork.PersonRepository.GetPeopleByNames(preferredNames.Union(alternativeNames).ToList()); + var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople); + + var modified = false; + foreach (var mapping in nameMappings) + { + mapping.Staff.Name = mapping.PreferredName; + + if (existingPeopleDictionary.ContainsKey(mapping.PreferredName.ToNormalized())) + { + continue; + } + + + if (existingPeopleDictionary.TryGetValue(mapping.AlternativeName.ToNormalized(), out var person)) + { + modified = true; + person.Aliases.Add(new PersonAliasBuilder(mapping.PreferredName).Build()); + } + } + + if (modified) + { + await _unitOfWork.CommitAsync(); + } + + return [.. staff]; + } + private static void GenerateGenreAndTagLists(ExternalSeriesDetailDto externalMetadata, MetadataSettingsDto settings, ref List processedTags, ref List processedGenres) { @@ -750,7 +792,7 @@ public class ExternalMetadataService : IExternalMetadataService var characters = externalCharacters .Select(w => new PersonDto() { - Name = w.Name, + Name = w.Name.Trim(), AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListCharacterWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) @@ -831,7 +873,7 @@ public class ExternalMetadataService : IExternalMetadataService var artists = upstreamArtists .Select(w => new PersonDto() { - Name = w.Name, + Name = w.Name.Trim(), AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) @@ -887,7 +929,7 @@ public class ExternalMetadataService : IExternalMetadataService var writers = upstreamWriters .Select(w => new PersonDto() { - Name = w.Name, + Name = w.Name.Trim(), AniListId = ScrobblingService.ExtractId(w.Url, ScrobblingService.AniListStaffWebsite), Description = StringHelper.CorrectUrls(StringHelper.RemoveSourceInDescription(StringHelper.SquashBreaklines(w.Description))), }) @@ -1085,7 +1127,7 @@ public class ExternalMetadataService : IExternalMetadataService madeModification = await UpdateChapterPeople(chapter, settings, PersonRole.Writer, potentialMatch.Writers) || madeModification; madeModification = await UpdateChapterCoverImage(chapter, settings, potentialMatch.CoverImageUrl) || madeModification; - madeModification = UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification; + madeModification = await UpdateExternalChapterMetadata(chapter, settings, potentialMatch) || madeModification; _unitOfWork.ChapterRepository.Update(chapter); await _unitOfWork.CommitAsync(); @@ -1094,7 +1136,7 @@ public class ExternalMetadataService : IExternalMetadataService return madeModification; } - private bool UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata) + private async Task UpdateExternalChapterMetadata(Chapter chapter, MetadataSettingsDto settings, ExternalChapterDto metadata) { if (!settings.Enabled) return false; @@ -1106,7 +1148,12 @@ public class ExternalMetadataService : IExternalMetadataService var madeModification = false; #region Review - _unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalReviews); + + // Remove existing Reviews + var existingReviews = await _unitOfWork.ChapterRepository.GetExternalChapterReview(chapter.Id); + _unitOfWork.ExternalSeriesMetadataRepository.Remove(existingReviews); + + List externalReviews = []; externalReviews.AddRange(metadata.CriticReviews .Where(r => !string.IsNullOrWhiteSpace(r.Username) && !string.IsNullOrWhiteSpace(r.Body)) @@ -1139,7 +1186,9 @@ public class ExternalMetadataService : IExternalMetadataService var averageCriticRating = metadata.CriticReviews.Average(r => r.Rating); var averageUserRating = metadata.UserReviews.Average(r => r.Rating); - _unitOfWork.ExternalSeriesMetadataRepository.Remove(chapter.ExternalRatings); + var existingRatings = await _unitOfWork.ChapterRepository.GetExternalChapterRatings(chapter.Id); + _unitOfWork.ExternalSeriesMetadataRepository.Remove(existingRatings); + chapter.ExternalRatings = [ new ExternalRating @@ -1304,7 +1353,7 @@ public class ExternalMetadataService : IExternalMetadataService var people = staff! .Select(w => new PersonDto() { - Name = w, + Name = w.Trim(), }) .Concat(chapter.People .Where(p => p.Role == role) diff --git a/API/Services/Plus/WantToReadSyncService.cs b/API/Services/Plus/WantToReadSyncService.cs index 07861710c..a6d536911 100644 --- a/API/Services/Plus/WantToReadSyncService.cs +++ b/API/Services/Plus/WantToReadSyncService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; +using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Recommendation; using API.DTOs.SeriesDetail; using API.Entities; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index b51ed2df6..426a8de3f 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -7,6 +7,7 @@ using API.Comparators; using API.Data; using API.Data.Repositories; using API.DTOs; +using API.DTOs.Person; using API.DTOs.SeriesDetail; using API.Entities; using API.Entities.Enums; @@ -361,8 +362,7 @@ public class SeriesService : ISeriesService var existingPeople = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedNames); // Use a dictionary for quick lookups - var existingPeopleDictionary = existingPeople.DistinctBy(p => p.NormalizedName) - .ToDictionary(p => p.NormalizedName, p => p); + var existingPeopleDictionary = PersonHelper.ConstructNameAndAliasDictionary(existingPeople); // List to track people that will be added to the metadata var peopleToAdd = new List(); @@ -450,7 +450,7 @@ public class SeriesService : ISeriesService try { var chapterMappings = - await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync(seriesIds.ToArray()); + await _unitOfWork.SeriesRepository.GetChapterIdWithSeriesIdForSeriesAsync([.. seriesIds]); var allChapterIds = new List(); foreach (var mapping in chapterMappings) @@ -458,9 +458,8 @@ public class SeriesService : ISeriesService allChapterIds.AddRange(mapping.Value); } - // NOTE: This isn't getting all the people and whatnot currently + // NOTE: This isn't getting all the people and whatnot currently due to the lack of includes var series = await _unitOfWork.SeriesRepository.GetSeriesByIdsAsync(seriesIds); - _unitOfWork.SeriesRepository.Remove(series); var libraryIds = series.Select(s => s.LibraryId); @@ -481,7 +480,8 @@ public class SeriesService : ISeriesService await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters(); await _unitOfWork.CollectionTagRepository.RemoveCollectionsWithoutSeries(); - _taskScheduler.CleanupChapters(allChapterIds.ToArray()); + _taskScheduler.CleanupChapters([.. allChapterIds]); + return true; } catch (Exception ex) diff --git a/API/Services/Tasks/Metadata/CoverDbService.cs b/API/Services/Tasks/Metadata/CoverDbService.cs index cbcff9284..59f01de55 100644 --- a/API/Services/Tasks/Metadata/CoverDbService.cs +++ b/API/Services/Tasks/Metadata/CoverDbService.cs @@ -81,6 +81,22 @@ public class CoverDbService : ICoverDbService _eventHub = eventHub; } + /// + /// Downloads the favicon image from a given website URL, optionally falling back to a custom method if standard methods fail. + /// + /// The full URL of the website to extract the favicon from. + /// The desired image encoding format for saving the favicon (e.g., WebP, PNG). + /// + /// A string representing the filename of the downloaded favicon image, saved to the configured favicon directory. + /// + /// + /// Thrown when favicon retrieval fails or if a previously failed domain is detected in cache. + /// + /// + /// This method first checks for a cached failure to avoid re-requesting bad links. + /// It then attempts to parse HTML for `link` tags pointing to `.png` favicons and + /// falls back to an internal fallback method if needed. Valid results are saved to disk. + /// public async Task DownloadFaviconAsync(string url, EncodeFormat encodeFormat) { // Parse the URL to get the domain (including subdomain) @@ -157,23 +173,10 @@ public class CoverDbService : ICoverDbService // Create the destination file path using var image = Image.PngloadStream(faviconStream); var filename = ImageService.GetWebLinkFormat(baseUrl, encodeFormat); - switch (encodeFormat) - { - case EncodeFormat.PNG: - image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - case EncodeFormat.WEBP: - image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - case EncodeFormat.AVIF: - image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } - + image.WriteToFile(Path.Combine(_directoryService.FaviconDirectory, filename)); _logger.LogDebug("Favicon for {Domain} downloaded and saved successfully", domain); + return filename; } catch (Exception ex) { @@ -212,23 +215,10 @@ public class CoverDbService : ICoverDbService // Create the destination file path using var image = Image.NewFromStream(publisherStream); var filename = ImageService.GetPublisherFormat(publisherName, encodeFormat); - switch (encodeFormat) - { - case EncodeFormat.PNG: - image.Pngsave(Path.Combine(_directoryService.PublisherDirectory, filename)); - break; - case EncodeFormat.WEBP: - image.Webpsave(Path.Combine(_directoryService.PublisherDirectory, filename)); - break; - case EncodeFormat.AVIF: - image.Heifsave(Path.Combine(_directoryService.PublisherDirectory, filename)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } - + image.WriteToFile(Path.Combine(_directoryService.PublisherDirectory, filename)); _logger.LogDebug("Publisher image for {PublisherName} downloaded and saved successfully", publisherName.Sanitize()); + return filename; } catch (Exception ex) { @@ -294,40 +284,30 @@ public class CoverDbService : ICoverDbService return null; } - private async Task DownloadImageFromUrl(string filenameWithoutExtension, EncodeFormat encodeFormat, string url) + private async Task DownloadImageFromUrl(string filenameWithoutExtension, EncodeFormat encodeFormat, string url, string? targetDirectory = null) { + // TODO: I need to unit test this to ensure it works when overwriting, etc + + // Target Directory defaults to CoverImageDirectory, but can be temp for when comparison between images is used + targetDirectory ??= _directoryService.CoverImageDirectory; + // Create the destination file path var filename = filenameWithoutExtension + encodeFormat.GetExtension(); - var targetFile = Path.Combine(_directoryService.CoverImageDirectory, filename); - - // Ensure if file exists, we delete to overwrite + var targetFile = Path.Combine(targetDirectory, filename); _logger.LogTrace("Fetching person image from {Url}", url.Sanitize()); // Download the file using Flurl - var personStream = await url + var imageStream = await url .AllowHttpStatus("2xx,304") .GetStreamAsync(); - using var image = Image.NewFromStream(personStream); - switch (encodeFormat) - { - case EncodeFormat.PNG: - image.Pngsave(targetFile); - break; - case EncodeFormat.WEBP: - image.Webpsave(targetFile); - break; - case EncodeFormat.AVIF: - image.Heifsave(targetFile); - break; - default: - throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null); - } + using var image = Image.NewFromStream(imageStream); + image.WriteToFile(targetFile); return filename; } - private async Task GetCoverPersonImagePath(Person person) + private async Task GetCoverPersonImagePath(Person person) { var tempFile = Path.Join(_directoryService.LongTermCacheDirectory, "people.yml"); @@ -384,25 +364,22 @@ public class CoverDbService : ICoverDbService await CacheDataAsync(urlsFileName, allOverrides); - if (!string.IsNullOrEmpty(allOverrides)) + if (string.IsNullOrEmpty(allOverrides)) return correctSizeLink; + + var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty); + var externalFile = allOverrides + .Split("\n") + .FirstOrDefault(url => + cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) || + cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty) + )); + + if (string.IsNullOrEmpty(externalFile)) { - var cleanedBaseUrl = baseUrl.Replace("https://", string.Empty); - var externalFile = allOverrides - .Split("\n") - .FirstOrDefault(url => - cleanedBaseUrl.Equals(url.Replace(".png", string.Empty)) || - cleanedBaseUrl.Replace("www.", string.Empty).Equals(url.Replace(".png", string.Empty) - )); - - if (string.IsNullOrEmpty(externalFile)) - { - throw new KavitaException($"Could not grab favicon from {baseUrl.Sanitize()}"); - } - - correctSizeLink = $"{NewHost}favicons/" + externalFile; + throw new KavitaException($"Could not grab favicon from {baseUrl.Sanitize()}"); } - return correctSizeLink; + return $"{NewHost}favicons/{externalFile}"; } private async Task FallbackToKavitaReaderPublisher(string publisherName) @@ -415,34 +392,30 @@ public class CoverDbService : ICoverDbService // Cache immediately await CacheDataAsync(publisherFileName, allOverrides); + if (string.IsNullOrEmpty(allOverrides)) return externalLink; - if (!string.IsNullOrEmpty(allOverrides)) - { - var externalFile = allOverrides - .Split("\n") - .Select(publisherLine => - { - var tokens = publisherLine.Split("|"); - if (tokens.Length != 2) return null; - var aliases = tokens[0]; - // Multiple publisher aliases are separated by # - if (aliases.Split("#").Any(name => name.ToLowerInvariant().Trim().Equals(publisherName.ToLowerInvariant().Trim()))) - { - return tokens[1]; - } - return null; - }) - .FirstOrDefault(url => !string.IsNullOrEmpty(url)); - - if (string.IsNullOrEmpty(externalFile)) + var externalFile = allOverrides + .Split("\n") + .Select(publisherLine => { - throw new KavitaException($"Could not grab publisher image for {publisherName}"); - } + var tokens = publisherLine.Split("|"); + if (tokens.Length != 2) return null; + var aliases = tokens[0]; + // Multiple publisher aliases are separated by # + if (aliases.Split("#").Any(name => name.ToLowerInvariant().Trim().Equals(publisherName.ToLowerInvariant().Trim()))) + { + return tokens[1]; + } + return null; + }) + .FirstOrDefault(url => !string.IsNullOrEmpty(url)); - externalLink = $"{NewHost}publishers/" + externalFile; + if (string.IsNullOrEmpty(externalFile)) + { + throw new KavitaException($"Could not grab publisher image for {publisherName}"); } - return externalLink; + return $"{NewHost}publishers/{externalLink}"; } private async Task CacheDataAsync(string fileName, string? content) @@ -485,33 +458,67 @@ public class CoverDbService : ICoverDbService /// Will check against all known null image placeholders to avoid writing it public async Task SetPersonCoverByUrl(Person person, string url, bool fromBase64 = true, bool checkNoImagePlaceholder = false) { - // TODO: Refactor checkNoImagePlaceholder bool to an action that evaluates how to process Image if (!string.IsNullOrEmpty(url)) { - var filePath = await CreateThumbnail(url, $"{ImageService.GetPersonFormat(person.Id)}", fromBase64); + var tempDir = _directoryService.TempDirectory; + var format = ImageService.GetPersonFormat(person.Id); + var finalFileName = format + ".webp"; + var tempFileName = format + "_new"; + var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir); - // Additional check to see if downloaded image is similar and we have a higher resolution - if (checkNoImagePlaceholder) + if (!string.IsNullOrEmpty(tempFilePath)) { - var matchRating = Path.Join(_directoryService.AssetsDirectory, "anilist-no-image-placeholder.jpg").GetSimilarity(Path.Join(_directoryService.CoverImageDirectory, filePath))!; + var tempFullPath = Path.Combine(tempDir, tempFilePath); + var finalFullPath = Path.Combine(_directoryService.CoverImageDirectory, finalFileName); - if (matchRating >= 0.9f) + // Skip setting image if it's similar to a known placeholder + if (checkNoImagePlaceholder) { - if (string.IsNullOrEmpty(person.CoverImage)) + var placeholderPath = Path.Combine(_directoryService.AssetsDirectory, "anilist-no-image-placeholder.jpg"); + var similarity = placeholderPath.CalculateSimilarity(tempFullPath); + if (similarity >= 0.9f) { - filePath = null; + _logger.LogInformation("Skipped setting placeholder image for person {PersonId} due to high similarity ({Similarity})", person.Id, similarity); + _directoryService.DeleteFiles([tempFullPath]); + return; + } + } + + try + { + if (!string.IsNullOrEmpty(person.CoverImage)) + { + var existingPath = Path.Combine(_directoryService.CoverImageDirectory, person.CoverImage); + var betterImage = existingPath.GetBetterImage(tempFullPath)!; + + var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); + if (choseNewImage) + { + _directoryService.DeleteFiles([existingPath]); + _directoryService.CopyFile(tempFullPath, finalFullPath); + person.CoverImage = finalFileName; + } + else + { + _directoryService.DeleteFiles([tempFullPath]); + return; + } } else { - filePath = Path.GetFileName(Path.Join(_directoryService.CoverImageDirectory, person.CoverImage)); + _directoryService.CopyFile(tempFullPath, finalFullPath); + person.CoverImage = finalFileName; } - } - } + catch (Exception ex) + { + _logger.LogError(ex, "Error choosing better image for Person: {PersonId}", person.Id); + _directoryService.CopyFile(tempFullPath, finalFullPath); + person.CoverImage = finalFileName; + } + + _directoryService.DeleteFiles([tempFullPath]); - if (!string.IsNullOrEmpty(filePath)) - { - person.CoverImage = filePath; person.CoverImageLocked = true; _imageService.UpdateColorScape(person); _unitOfWork.PersonRepository.Update(person); @@ -544,31 +551,52 @@ public class CoverDbService : ICoverDbService { if (!string.IsNullOrEmpty(url)) { - var filePath = await CreateThumbnail(url, $"{ImageService.GetSeriesFormat(series.Id)}", fromBase64); + var tempDir = _directoryService.TempDirectory; + var format = ImageService.GetSeriesFormat(series.Id); + var finalFileName = format + ".webp"; + var tempFileName = format + "_new"; + var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDir); - if (!string.IsNullOrEmpty(filePath)) + if (!string.IsNullOrEmpty(tempFilePath)) { - // Additional check to see if downloaded image is similar and we have a higher resolution + var tempFullPath = Path.Combine(tempDir, tempFilePath); + var finalFullPath = Path.Combine(_directoryService.CoverImageDirectory, finalFileName); + if (chooseBetterImage && !string.IsNullOrEmpty(series.CoverImage)) { try { - var betterImage = Path.Join(_directoryService.CoverImageDirectory, series.CoverImage) - .GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!; - filePath = Path.GetFileName(betterImage); + var existingPath = Path.Combine(_directoryService.CoverImageDirectory, series.CoverImage); + var betterImage = existingPath.GetBetterImage(tempFullPath)!; + + var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); + if (choseNewImage) + { + _directoryService.DeleteFiles([existingPath]); + _directoryService.CopyFile(tempFullPath, finalFullPath); + series.CoverImage = finalFileName; + } + else + { + _directoryService.DeleteFiles([tempFullPath]); + return; + } } catch (Exception ex) { - _logger.LogError(ex, "There was an issue trying to choose a better cover image for Series: {SeriesName} ({SeriesId})", series.Name, series.Id); + _logger.LogError(ex, "Error choosing better image for Series: {SeriesId}", series.Id); + _directoryService.CopyFile(tempFullPath, finalFullPath); + series.CoverImage = finalFileName; } } - - series.CoverImage = filePath; - series.CoverImageLocked = true; - if (series.CoverImage == null) + else { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null"); + _directoryService.CopyFile(tempFullPath, finalFullPath); + series.CoverImage = finalFileName; } + + _directoryService.DeleteFiles([tempFullPath]); + series.CoverImageLocked = true; _imageService.UpdateColorScape(series); _unitOfWork.SeriesRepository.Update(series); } @@ -577,10 +605,7 @@ public class CoverDbService : ICoverDbService { series.CoverImage = null; series.CoverImageLocked = false; - if (series.CoverImage == null) - { - _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null"); - } + _logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null"); _imageService.UpdateColorScape(series); _unitOfWork.SeriesRepository.Update(series); } @@ -597,26 +622,53 @@ public class CoverDbService : ICoverDbService { if (!string.IsNullOrEmpty(url)) { - var filePath = await CreateThumbnail(url, $"{ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId)}", fromBase64); + var tempDirectory = _directoryService.TempDirectory; + var finalFileName = ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId) + ".webp"; + var tempFileName = ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId) + "_new"; - if (!string.IsNullOrEmpty(filePath)) + var tempFilePath = await CreateThumbnail(url, tempFileName, fromBase64, tempDirectory); + + if (!string.IsNullOrEmpty(tempFilePath)) { - // Additional check to see if downloaded image is similar and we have a higher resolution + var tempFullPath = Path.Combine(tempDirectory, tempFilePath); + var finalFullPath = Path.Combine(_directoryService.CoverImageDirectory, finalFileName); + if (chooseBetterImage && !string.IsNullOrEmpty(chapter.CoverImage)) { try { - var betterImage = Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage) - .GetBetterImage(Path.Join(_directoryService.CoverImageDirectory, filePath))!; - filePath = Path.GetFileName(betterImage); + var existingPath = Path.Combine(_directoryService.CoverImageDirectory, chapter.CoverImage); + var betterImage = existingPath.GetBetterImage(tempFullPath)!; + var choseNewImage = string.Equals(betterImage, tempFullPath, StringComparison.OrdinalIgnoreCase); + + if (choseNewImage) + { + // This will fail if Cover gen is done just before this as there is a bug with files getting locked. + _directoryService.DeleteFiles([existingPath]); + _directoryService.CopyFile(tempFullPath, finalFullPath); + _directoryService.DeleteFiles([tempFullPath]); + } + else + { + _directoryService.DeleteFiles([tempFullPath]); + return; + } + + chapter.CoverImage = finalFileName; } catch (Exception ex) { _logger.LogError(ex, "There was an issue trying to choose a better cover image for Chapter: {FileName} ({ChapterId})", chapter.Range, chapter.Id); } } + else + { + // No comparison needed, just copy and rename to final + _directoryService.CopyFile(tempFullPath, finalFullPath); + _directoryService.DeleteFiles([tempFullPath]); + chapter.CoverImage = finalFileName; + } - chapter.CoverImage = filePath; chapter.CoverImageLocked = true; _imageService.UpdateColorScape(chapter); _unitOfWork.ChapterRepository.Update(chapter); @@ -633,13 +685,26 @@ public class CoverDbService : ICoverDbService if (_unitOfWork.HasChanges()) { await _unitOfWork.CommitAsync(); - await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, - MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false); + await _eventHub.SendMessageAsync( + MessageFactory.CoverUpdate, + MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), + false + ); } } - private async Task CreateThumbnail(string url, string filename, bool fromBase64 = true) + /// + /// + /// + /// + /// Filename without extension + /// + /// Not useable with fromBase64. Allows a different directory to be written to + /// + private async Task CreateThumbnail(string url, string filenameWithoutExtension, bool fromBase64 = true, string? targetDirectory = null) { + targetDirectory ??= _directoryService.CoverImageDirectory; + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var encodeFormat = settings.EncodeMediaAs; var coverImageSize = settings.CoverImageSize; @@ -647,9 +712,9 @@ public class CoverDbService : ICoverDbService if (fromBase64) { return _imageService.CreateThumbnailFromBase64(url, - filename, encodeFormat, coverImageSize.GetDimensions().Width); + filenameWithoutExtension, encodeFormat, coverImageSize.GetDimensions().Width); } - return await DownloadImageFromUrl(filename, encodeFormat, url); + return await DownloadImageFromUrl(filenameWithoutExtension, encodeFormat, url, targetDirectory); } } diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index d2e6437a3..fec0304a8 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -310,7 +310,7 @@ public class LibraryWatcher : ILibraryWatcher if (rootFolder.Count == 0) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. - return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[rootFolder.Count - 1])); + return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder[^1])); } diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 59721fe61..454c72733 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -194,8 +194,8 @@ public class ProcessSeries : IProcessSeries if (seriesAdded) { // See if any recommendations can link up to the series and pre-fetch external metadata for the series - BackgroundJob.Enqueue(() => - _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type)); + // BackgroundJob.Enqueue(() => + // _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type)); await _eventHub.SendMessageAsync(MessageFactory.SeriesAdded, MessageFactory.SeriesAddedEvent(series.Id, series.Name, series.LibraryId), false); @@ -214,6 +214,10 @@ public class ProcessSeries : IProcessSeries return; } + if (seriesAdded) + { + await _externalMetadataService.FetchSeriesMetadata(series.Id, series.Library.Type); + } await _metadataService.GenerateCoversForSeries(series.LibraryId, series.Id, false, false); await _wordCountAnalyzerService.ScanSeries(series.LibraryId, series.Id, forceUpdate); } diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 123b610ff..4ccf79abb 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -52,6 +52,7 @@ public interface IVersionUpdaterService Task PushUpdate(UpdateNotificationDto update); Task> GetAllReleases(int count = 0); Task GetNumberOfReleasesBehind(bool stableOnly = false); + void BustGithubCache(); } @@ -384,7 +385,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc <= CacheDuration) { var cachedData = await File.ReadAllTextAsync(_cacheLatestReleaseFilePath); - return System.Text.Json.JsonSerializer.Deserialize(cachedData); + return JsonSerializer.Deserialize(cachedData); } return null; @@ -407,7 +408,7 @@ public partial class VersionUpdaterService : IVersionUpdaterService { try { - var json = System.Text.Json.JsonSerializer.Serialize(update, JsonOptions); + var json = JsonSerializer.Serialize(update, JsonOptions); await File.WriteAllTextAsync(_cacheLatestReleaseFilePath, json); } catch (Exception ex) @@ -446,6 +447,21 @@ public partial class VersionUpdaterService : IVersionUpdaterService .Count(u => u.IsReleaseNewer); } + /// + /// Clears the Github cache + /// + public void BustGithubCache() + { + try + { + File.Delete(_cacheFilePath); + File.Delete(_cacheLatestReleaseFilePath); + } catch (Exception ex) + { + _logger.LogError(ex, "Failed to clear Github cache"); + } + } + private UpdateNotificationDto? CreateDto(GithubReleaseMetadata? update) { if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null; diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index de9818b79..ba967d8a6 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -1,5 +1,6 @@ using System; using API.DTOs.Update; +using API.Entities.Person; using API.Extensions; using API.Services.Plus; @@ -147,6 +148,10 @@ public static class MessageFactory /// Volume is removed from server /// public const string VolumeRemoved = "VolumeRemoved"; + /// + /// A Person merged has been merged into another + /// + public const string PersonMerged = "PersonMerged"; public static SignalRMessage DashboardUpdateEvent(int userId) { @@ -661,4 +666,17 @@ public static class MessageFactory EventType = ProgressEventType.Single, }; } + + public static SignalRMessage PersonMergedMessage(Person dst, Person src) + { + return new SignalRMessage() + { + Name = PersonMerged, + Body = new + { + srcId = src.Id, + dstName = dst.Name, + }, + }; + } } diff --git a/API/Startup.cs b/API/Startup.cs index 188c2b2dd..cb32d1742 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -55,6 +55,9 @@ public class Startup { _config = config; _env = env; + + // Disable Hangfire Automatic Retry + GlobalJobFilters.Filters.Add(new AutomaticRetryAttribute { Attempts = 0 }); } // This method gets called by the runtime. Use this method to add services to the container. @@ -138,8 +141,8 @@ public class Startup { c.SwaggerDoc("v1", new OpenApiInfo { - Version = "3.1.0", - Title = $"Kavita (v{BuildInfo.Version})", + Version = BuildInfo.Version.ToString(), + Title = $"Kavita", Description = $"Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v{BuildInfo.Version}", License = new OpenApiLicense { @@ -223,7 +226,7 @@ public class Startup // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ICacheService cacheService, - IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService) + IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService, IVersionUpdaterService versionService) { var logger = serviceProvider.GetRequiredService>(); @@ -235,9 +238,10 @@ public class Startup // Apply all migrations on startup var dataContext = serviceProvider.GetRequiredService(); - logger.LogInformation("Running Migrations"); + #region Migrations + // v0.7.9 await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger); @@ -289,13 +293,23 @@ public class Startup await ManualMigrateScrobbleSpecials.Migrate(dataContext, logger); await ManualMigrateScrobbleEventGen.Migrate(dataContext, logger); + #endregion + // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); + var isVersionDifferent = installVersion.Value != BuildInfo.Version.ToString(); installVersion.Value = BuildInfo.Version.ToString(); unitOfWork.SettingsRepository.Update(installVersion); await unitOfWork.CommitAsync(); logger.LogInformation("Running Migrations - complete"); + + if (isVersionDifferent) + { + // Clear the Github cache so update stuff shows correctly + versionService.BustGithubCache(); + } + }).GetAwaiter() .GetResult(); } diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 628692006..ac49db155 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -3,7 +3,7 @@ net9.0 kavitareader.com Kavita - 0.8.6.6 + 0.8.6.10 en true @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/_series-detail-common.scss b/UI/Web/src/_series-detail-common.scss index f043dec17..efb54f860 100644 --- a/UI/Web/src/_series-detail-common.scss +++ b/UI/Web/src/_series-detail-common.scss @@ -13,7 +13,7 @@ } .subtitle { - color: lightgrey; + color: var(--detail-subtitle-color); font-weight: bold; font-size: 0.8rem; } diff --git a/UI/Web/src/app/_models/library/library.ts b/UI/Web/src/app/_models/library/library.ts index 74cabc658..06ba86cf2 100644 --- a/UI/Web/src/app/_models/library/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -6,6 +6,9 @@ export enum LibraryType { Book = 2, Images = 3, LightNovel = 4, + /** + * Comic (Legacy) + */ ComicVine = 5 } diff --git a/UI/Web/src/app/_models/metadata/person.ts b/UI/Web/src/app/_models/metadata/person.ts index c8a4c566e..6b098de19 100644 --- a/UI/Web/src/app/_models/metadata/person.ts +++ b/UI/Web/src/app/_models/metadata/person.ts @@ -22,6 +22,7 @@ export interface Person extends IHasCover { id: number; name: string; description: string; + aliases: Array; coverImage?: string; coverImageLocked: boolean; malId?: number; diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 6d2f7053e..61fee39ec 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -7,12 +7,13 @@ import {Library} from '../_models/library/library'; import {ReadingList} from '../_models/reading-list'; import {Series} from '../_models/series'; import {Volume} from '../_models/volume'; -import {AccountService} from './account.service'; +import {AccountService, Role} from './account.service'; import {DeviceService} from './device.service'; import {SideNavStream} from "../_models/sidenav/sidenav-stream"; import {SmartFilter} from "../_models/metadata/v2/smart-filter"; import {translate} from "@jsverse/transloco"; import {Person} from "../_models/metadata/person"; +import {User} from '../_models/user'; export enum Action { Submenu = -1, @@ -106,7 +107,7 @@ export enum Action { Promote = 24, UnPromote = 25, /** - * Invoke a refresh covers as false to generate colorscapes + * Invoke refresh covers as false to generate colorscapes */ GenerateColorScape = 26, /** @@ -116,20 +117,31 @@ export enum Action { /** * Match an entity with an upstream system */ - Match = 28 + Match = 28, + /** + * Merge two (or more?) entities + */ + Merge = 29, } /** * Callback for an action */ -export type ActionCallback = (action: ActionItem, data: T) => void; -export type ActionAllowedCallback = (action: ActionItem) => boolean; +export type ActionCallback = (action: ActionItem, entity: T) => void; +export type ActionShouldRenderFunc = (action: ActionItem, entity: T, user: User) => boolean; export interface ActionItem { title: string; description: string; action: Action; callback: ActionCallback; + /** + * Roles required to be present for ActionItem to show. If empty, assumes anyone can see. At least one needs to apply. + */ + requiredRoles: Role[]; + /** + * @deprecated Use required Roles instead + */ requiresAdmin: boolean; children: Array>; /** @@ -145,94 +157,98 @@ export interface ActionItem { * Extra data that needs to be sent back from the card item. Used mainly for dynamicList. This will be the item from dyanamicList return */ _extra?: {title: string, data: any}; + /** + * Will call on each action to determine if it should show for the appropriate entity based on state and user + */ + shouldRender: ActionShouldRenderFunc; } +/** + * Entities that can be actioned upon + */ +export type ActionableEntity = Volume | Series | Chapter | ReadingList | UserCollection | Person | Library | SideNavStream | SmartFilter | null; + @Injectable({ providedIn: 'root', }) export class ActionFactoryService { - libraryActions: Array> = []; - - seriesActions: Array> = []; - - volumeActions: Array> = []; - - chapterActions: Array> = []; - - collectionTagActions: Array> = []; - - readingListActions: Array> = []; - - bookmarkActions: Array> = []; - + private libraryActions: Array> = []; + private seriesActions: Array> = []; + private volumeActions: Array> = []; + private chapterActions: Array> = []; + private collectionTagActions: Array> = []; + private readingListActions: Array> = []; + private bookmarkActions: Array> = []; private personActions: Array> = []; - - sideNavStreamActions: Array> = []; - smartFilterActions: Array> = []; - - sideNavHomeActions: Array> = []; - - isAdmin = false; - + private sideNavStreamActions: Array> = []; + private smartFilterActions: Array> = []; + private sideNavHomeActions: Array> = []; constructor(private accountService: AccountService, private deviceService: DeviceService) { - this.accountService.currentUser$.subscribe((user) => { - if (user) { - this.isAdmin = this.accountService.hasAdminRole(user); - } else { - this._resetActions(); - return; // If user is logged out, we don't need to do anything - } - + this.accountService.currentUser$.subscribe((_) => { this._resetActions(); }); } - getLibraryActions(callback: ActionCallback) { - return this.applyCallbackToList(this.libraryActions, callback); + getLibraryActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.libraryActions, callback, shouldRenderFunc) as ActionItem[]; } - getSeriesActions(callback: ActionCallback) { - return this.applyCallbackToList(this.seriesActions, callback); + getSeriesActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { + return this.applyCallbackToList(this.seriesActions, callback, shouldRenderFunc); } - getSideNavStreamActions(callback: ActionCallback) { - return this.applyCallbackToList(this.sideNavStreamActions, callback); + getSideNavStreamActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.sideNavStreamActions, callback, shouldRenderFunc); } - getSmartFilterActions(callback: ActionCallback) { - return this.applyCallbackToList(this.smartFilterActions, callback); + getSmartFilterActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.smartFilterActions, callback, shouldRenderFunc); } - getVolumeActions(callback: ActionCallback) { - return this.applyCallbackToList(this.volumeActions, callback); + getVolumeActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { + return this.applyCallbackToList(this.volumeActions, callback, shouldRenderFunc); } - getChapterActions(callback: ActionCallback) { - return this.applyCallbackToList(this.chapterActions, callback); + getChapterActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.basicReadRender) { + return this.applyCallbackToList(this.chapterActions, callback, shouldRenderFunc); } - getCollectionTagActions(callback: ActionCallback) { - return this.applyCallbackToList(this.collectionTagActions, callback); + getCollectionTagActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.collectionTagActions, callback, shouldRenderFunc); } - getReadingListActions(callback: ActionCallback) { - return this.applyCallbackToList(this.readingListActions, callback); + getReadingListActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.readingListActions, callback, shouldRenderFunc); } - getBookmarkActions(callback: ActionCallback) { - return this.applyCallbackToList(this.bookmarkActions, callback); + getBookmarkActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.bookmarkActions, callback, shouldRenderFunc); } - getPersonActions(callback: ActionCallback) { - return this.applyCallbackToList(this.personActions, callback); + getPersonActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.personActions, callback, shouldRenderFunc); } - getSideNavHomeActions(callback: ActionCallback) { - return this.applyCallbackToList(this.sideNavHomeActions, callback); + getSideNavHomeActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { + return this.applyCallbackToList(this.sideNavHomeActions, callback, shouldRenderFunc); } - dummyCallback(action: ActionItem, data: any) {} + dummyCallback(action: ActionItem, entity: any) {} + dummyShouldRender(action: ActionItem, entity: any, user: User) {return true;} + basicReadRender(action: ActionItem, entity: any, user: User) { + if (entity === null || entity === undefined) return true; + if (!entity.hasOwnProperty('pagesRead') && !entity.hasOwnProperty('pages')) return true; + + switch (action.action) { + case(Action.MarkAsRead): + return entity.pagesRead < entity.pages; + case(Action.MarkAsUnread): + return entity.pagesRead !== 0; + default: + return true; + } + } filterSendToAction(actions: Array>, chapter: Chapter) { // if (chapter.files.filter(f => f.format === MangaFormat.EPUB || f.format === MangaFormat.PDF).length !== chapter.files.length) { @@ -275,7 +291,7 @@ export class ActionFactoryService { return tasks.filter(t => !blacklist.includes(t.action)); } - getBulkLibraryActions(callback: ActionCallback) { + getBulkLibraryActions(callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender) { // Scan is currently not supported due to the backend not being able to handle it yet const actions = this.flattenActions(this.libraryActions).filter(a => { @@ -289,11 +305,13 @@ export class ActionFactoryService { dynamicList: undefined, action: Action.CopySettings, callback: this.dummyCallback, + shouldRender: shouldRenderFunc, children: [], + requiredRoles: [Role.Admin], requiresAdmin: true, title: 'copy-settings' }) - return this.applyCallbackToList(actions, callback); + return this.applyCallbackToList(actions, callback, shouldRenderFunc) as ActionItem[]; } flattenActions(actions: Array>): Array> { @@ -319,7 +337,9 @@ export class ActionFactoryService { title: 'scan-library', description: 'scan-library-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -327,14 +347,18 @@ export class ActionFactoryService { title: 'others', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [ { action: Action.RefreshMetadata, title: 'refresh-covers', description: 'refresh-covers-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -342,7 +366,9 @@ export class ActionFactoryService { title: 'generate-colorscape', description: 'generate-colorscape-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -350,7 +376,9 @@ export class ActionFactoryService { title: 'analyze-files', description: 'analyze-files-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -358,7 +386,9 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, ], @@ -368,7 +398,9 @@ export class ActionFactoryService { title: 'settings', description: 'settings-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, ]; @@ -379,7 +411,9 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -387,7 +421,9 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], class: 'danger', children: [], }, @@ -396,7 +432,9 @@ export class ActionFactoryService { title: 'promote', description: 'promote-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -404,7 +442,9 @@ export class ActionFactoryService { title: 'unpromote', description: 'unpromote-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -415,7 +455,9 @@ export class ActionFactoryService { title: 'mark-as-read', description: 'mark-as-read-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -423,7 +465,9 @@ export class ActionFactoryService { title: 'mark-as-unread', description: 'mark-as-unread-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -431,7 +475,9 @@ export class ActionFactoryService { title: 'scan-series', description: 'scan-series-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -439,14 +485,18 @@ export class ActionFactoryService { title: 'add-to', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.AddToWantToReadList, title: 'add-to-want-to-read', description: 'add-to-want-to-read-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -454,7 +504,9 @@ export class ActionFactoryService { title: 'remove-from-want-to-read', description: 'remove-to-want-to-read-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -462,7 +514,9 @@ export class ActionFactoryService { title: 'add-to-reading-list', description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -470,26 +524,11 @@ export class ActionFactoryService { title: 'add-to-collection', description: 'add-to-collection-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, - - // { - // action: Action.AddToScrobbleHold, - // title: 'add-to-scrobble-hold', - // description: 'add-to-scrobble-hold-tooltip', - // callback: this.dummyCallback, - // requiresAdmin: true, - // children: [], - // }, - // { - // action: Action.RemoveFromScrobbleHold, - // title: 'remove-from-scrobble-hold', - // description: 'remove-from-scrobble-hold-tooltip', - // callback: this.dummyCallback, - // requiresAdmin: true, - // children: [], - // }, ], }, { @@ -497,14 +536,18 @@ export class ActionFactoryService { title: 'send-to', description: 'send-to-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.SendTo, title: '', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -517,14 +560,18 @@ export class ActionFactoryService { title: 'others', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [], children: [ { action: Action.RefreshMetadata, title: 'refresh-covers', description: 'refresh-covers-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -532,7 +579,9 @@ export class ActionFactoryService { title: 'generate-colorscape', description: 'generate-colorscape-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -540,7 +589,9 @@ export class ActionFactoryService { title: 'analyze-files', description: 'analyze-files-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -548,7 +599,9 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], class: 'danger', children: [], }, @@ -559,7 +612,9 @@ export class ActionFactoryService { title: 'match', description: 'match-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -567,7 +622,9 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [Role.Download], children: [], }, { @@ -575,7 +632,9 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, ]; @@ -586,7 +645,9 @@ export class ActionFactoryService { title: 'read-incognito', description: 'read-incognito-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -594,7 +655,9 @@ export class ActionFactoryService { title: 'mark-as-read', description: 'mark-as-read-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -602,7 +665,9 @@ export class ActionFactoryService { title: 'mark-as-unread', description: 'mark-as-unread-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -610,14 +675,18 @@ export class ActionFactoryService { title: 'add-to', description: '=', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.AddToReadingList, title: 'add-to-reading-list', description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], } ] @@ -627,14 +696,18 @@ export class ActionFactoryService { title: 'send-to', description: 'send-to-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.SendTo, title: '', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -647,14 +720,18 @@ export class ActionFactoryService { title: 'others', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.Delete, title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -662,7 +739,9 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ] @@ -672,7 +751,9 @@ export class ActionFactoryService { title: 'details', description: 'edit-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -683,7 +764,9 @@ export class ActionFactoryService { title: 'read-incognito', description: 'read-incognito-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -691,7 +774,9 @@ export class ActionFactoryService { title: 'mark-as-read', description: 'mark-as-read-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -699,7 +784,9 @@ export class ActionFactoryService { title: 'mark-as-unread', description: 'mark-as-unread-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -707,14 +794,18 @@ export class ActionFactoryService { title: 'add-to', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.AddToReadingList, title: 'add-to-reading-list', description: 'add-to-reading-list-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], } ] @@ -724,14 +815,18 @@ export class ActionFactoryService { title: 'send-to', description: 'send-to-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.SendTo, title: '', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], dynamicList: this.deviceService.devices$.pipe(map((devices: Array) => devices.map(d => { return {'title': d.name, 'data': d}; }), shareReplay())), @@ -745,14 +840,18 @@ export class ActionFactoryService { title: 'others', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [ { action: Action.Delete, title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], }, { @@ -760,7 +859,9 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [Role.Download], children: [], }, ] @@ -770,7 +871,9 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -781,7 +884,9 @@ export class ActionFactoryService { title: 'edit', description: 'edit-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -789,7 +894,9 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], class: 'danger', children: [], }, @@ -798,7 +905,9 @@ export class ActionFactoryService { title: 'promote', description: 'promote-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -806,7 +915,9 @@ export class ActionFactoryService { title: 'unpromote', description: 'unpromote-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -817,7 +928,19 @@ export class ActionFactoryService { title: 'edit', description: 'edit-person-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: true, + requiredRoles: [Role.Admin], + children: [], + }, + { + action: Action.Merge, + title: 'merge', + description: 'merge-person-tooltip', + callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, + requiresAdmin: true, + requiredRoles: [Role.Admin], children: [], } ]; @@ -828,7 +951,9 @@ export class ActionFactoryService { title: 'view-series', description: 'view-series-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -836,7 +961,9 @@ export class ActionFactoryService { title: 'download', description: 'download-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -844,8 +971,10 @@ export class ActionFactoryService { title: 'clear', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, class: 'danger', requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -856,7 +985,9 @@ export class ActionFactoryService { title: 'mark-visible', description: 'mark-visible-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -864,7 +995,9 @@ export class ActionFactoryService { title: 'mark-invisible', description: 'mark-invisible-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -875,7 +1008,9 @@ export class ActionFactoryService { title: 'rename', description: 'rename-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, { @@ -883,7 +1018,9 @@ export class ActionFactoryService { title: 'delete', description: 'delete-tooltip', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], }, ]; @@ -894,7 +1031,9 @@ export class ActionFactoryService { title: 'reorder', description: '', callback: this.dummyCallback, + shouldRender: this.dummyShouldRender, requiresAdmin: false, + requiredRoles: [], children: [], } ] @@ -902,21 +1041,24 @@ export class ActionFactoryService { } - private applyCallback(action: ActionItem, callback: (action: ActionItem, data: any) => void) { + private applyCallback(action: ActionItem, callback: ActionCallback, shouldRenderFunc: ActionShouldRenderFunc) { action.callback = callback; + action.shouldRender = shouldRenderFunc; if (action.children === null || action.children?.length === 0) return; action.children?.forEach((childAction) => { - this.applyCallback(childAction, callback); + this.applyCallback(childAction, callback, shouldRenderFunc); }); } - public applyCallbackToList(list: Array>, callback: (action: ActionItem, data: any) => void): Array> { + public applyCallbackToList(list: Array>, + callback: ActionCallback, + shouldRenderFunc: ActionShouldRenderFunc = this.dummyShouldRender): Array> { const actions = list.map((a) => { return { ...a }; }); - actions.forEach((action) => this.applyCallback(action, callback)); + actions.forEach((action) => this.applyCallback(action, callback, shouldRenderFunc)); return actions; } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 1cf4e448e..37826b0e2 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -473,8 +473,7 @@ export class ActionService { } async deleteMultipleVolumes(volumes: Array, callback?: BooleanActionCallback) { - // TODO: Change translation key back to "toasts.confirm-delete-multiple-volumes" - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: volumes.length}))) return; + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-volumes', {count: volumes.length}))) return; this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => { if (callback) { @@ -536,7 +535,7 @@ export class ActionService { addMultipleSeriesToWantToReadList(seriesIds: Array, callback?: VoidActionCallback) { this.memberService.addSeriesToWantToRead(seriesIds).subscribe(() => { - this.toastr.success('Series added to Want to Read list'); + this.toastr.success(translate('toasts.series-added-want-to-read')); if (callback) { callback(); } diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index ea1819bd7..67f07f32e 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -109,7 +109,11 @@ export enum EVENTS { /** * A Progress event when a smart collection is synchronizing */ - SmartCollectionSync = 'SmartCollectionSync' + SmartCollectionSync = 'SmartCollectionSync', + /** + * A Person merged has been merged into another + */ + PersonMerged = 'PersonMerged', } export interface Message { @@ -336,6 +340,13 @@ export class MessageHubService { payload: resp.body }); }); + + this.hubConnection.on(EVENTS.PersonMerged, resp => { + this.messagesSource.next({ + event: EVENTS.PersonMerged, + payload: resp.body + }); + }) } stopHubConnection() { diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index 676aa6e71..0ac58b178 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -1,14 +1,12 @@ -import { Injectable } from '@angular/core'; -import { HttpClient, HttpParams } from "@angular/common/http"; +import {Injectable} from '@angular/core'; +import {HttpClient, HttpParams} from "@angular/common/http"; import {environment} from "../../environments/environment"; import {Person, PersonRole} from "../_models/metadata/person"; -import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; import {PaginatedResult} from "../_models/pagination"; import {Series} from "../_models/series"; import {map} from "rxjs/operators"; import {UtilityService} from "../shared/_services/utility.service"; import {BrowsePerson} from "../_models/person/browse-person"; -import {Chapter} from "../_models/chapter"; import {StandaloneChapter} from "../_models/standalone-chapter"; import {TextResonse} from "../_types/text-response"; @@ -29,6 +27,10 @@ export class PersonService { return this.httpClient.get(this.baseUrl + `person?name=${name}`); } + searchPerson(name: string) { + return this.httpClient.get>(this.baseUrl + `person/search?queryString=${encodeURIComponent(name)}`); + } + getRolesForPerson(personId: number) { return this.httpClient.get>(this.baseUrl + `person/roles?personId=${personId}`); } @@ -55,4 +57,15 @@ export class PersonService { downloadCover(personId: number) { return this.httpClient.post(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse); } + + isValidAlias(personId: number, alias: string) { + return this.httpClient.get(this.baseUrl + `person/valid-alias?personId=${personId}&alias=${alias}`, TextResonse).pipe( + map(valid => valid + '' === 'true') + ); + } + + mergePerson(destId: number, srcId: number) { + return this.httpClient.post(this.baseUrl + 'person/merge', {destId, srcId}); + } + } diff --git a/UI/Web/src/app/_services/statistics.service.ts b/UI/Web/src/app/_services/statistics.service.ts index f13b29c87..cf80765f2 100644 --- a/UI/Web/src/app/_services/statistics.service.ts +++ b/UI/Web/src/app/_services/statistics.service.ts @@ -1,20 +1,19 @@ -import { HttpClient } from '@angular/common/http'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {Inject, inject, Injectable} from '@angular/core'; -import { environment } from 'src/environments/environment'; -import { UserReadStatistics } from '../statistics/_models/user-read-statistics'; -import { PublicationStatusPipe } from '../_pipes/publication-status.pipe'; -import {asyncScheduler, finalize, map, tap} from 'rxjs'; -import { MangaFormatPipe } from '../_pipes/manga-format.pipe'; -import { FileExtensionBreakdown } from '../statistics/_models/file-breakdown'; -import { TopUserRead } from '../statistics/_models/top-reads'; -import { ReadHistoryEvent } from '../statistics/_models/read-history-event'; -import { ServerStatistics } from '../statistics/_models/server-statistics'; -import { StatCount } from '../statistics/_models/stat-count'; -import { PublicationStatus } from '../_models/metadata/publication-status'; -import { MangaFormat } from '../_models/manga-format'; -import { TextResonse } from '../_types/text-response'; +import {environment} from 'src/environments/environment'; +import {UserReadStatistics} from '../statistics/_models/user-read-statistics'; +import {PublicationStatusPipe} from '../_pipes/publication-status.pipe'; +import {asyncScheduler, map} from 'rxjs'; +import {MangaFormatPipe} from '../_pipes/manga-format.pipe'; +import {FileExtensionBreakdown} from '../statistics/_models/file-breakdown'; +import {TopUserRead} from '../statistics/_models/top-reads'; +import {ReadHistoryEvent} from '../statistics/_models/read-history-event'; +import {ServerStatistics} from '../statistics/_models/server-statistics'; +import {StatCount} from '../statistics/_models/stat-count'; +import {PublicationStatus} from '../_models/metadata/publication-status'; +import {MangaFormat} from '../_models/manga-format'; +import {TextResonse} from '../_types/text-response'; import {TranslocoService} from "@jsverse/transloco"; -import {KavitaPlusMetadataBreakdown} from "../statistics/_models/kavitaplus-metadata-breakdown"; import {throttleTime} from "rxjs/operators"; import {DEBOUNCE_TIME} from "../shared/_services/download.service"; import {download} from "../shared/_models/download"; @@ -44,11 +43,14 @@ export class StatisticsService { constructor(private httpClient: HttpClient, @Inject(SAVER) private save: Saver) { } getUserStatistics(userId: number, libraryIds: Array = []) { - // TODO: Convert to httpParams object - let url = 'stats/user/' + userId + '/read'; - if (libraryIds.length > 0) url += '?libraryIds=' + libraryIds.join(','); + const url = `${this.baseUrl}stats/user/${userId}/read`; - return this.httpClient.get(this.baseUrl + url); + let params = new HttpParams(); + if (libraryIds.length > 0) { + params = params.set('libraryIds', libraryIds.join(',')); + } + + return this.httpClient.get(url, { params }); } getServerStatistics() { @@ -59,7 +61,7 @@ export class StatisticsService { return this.httpClient.get[]>(this.baseUrl + 'stats/server/count/year').pipe( map(spreads => spreads.map(spread => { return {name: spread.value + '', value: spread.count}; - }))); + }))); } getTopYears() { diff --git a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html index 067dc5fb2..caf8bf683 100644 --- a/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html +++ b/UI/Web/src/app/_single-module/actionable-modal/actionable-modal.component.html @@ -1,7 +1,9 @@
-
{{item.name}}
+
+ {{item.name}} +
+ @if (item.aliases.length > 0) { + + {{t('person-aka-status')}} + + }
@@ -206,7 +213,7 @@ } } - + diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.scss b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.scss index 849925da0..a4b5eaffd 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.scss +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.scss @@ -149,3 +149,7 @@ } } } + +.small-text { + font-size: 0.8rem; +} diff --git a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html b/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html index 4782529b3..f6ae1c6ae 100644 --- a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html +++ b/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.html @@ -96,6 +96,19 @@ +
  • + {{t(TabID.Aliases)}} + +
    {{t('aliases-label')}}
    +
    {{t('aliases-tooltip')}}
    + +
    +
  • +
  • {{t(TabID.CoverImage)}} diff --git a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.ts b/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.ts index 7db41ce13..74a20e951 100644 --- a/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.ts +++ b/UI/Web/src/app/person-detail/_modal/edit-person-modal/edit-person-modal.component.ts @@ -1,6 +1,14 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service"; -import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; +import { + AbstractControl, + AsyncValidatorFn, + FormControl, + FormGroup, + ReactiveFormsModule, + ValidationErrors, + Validators +} from "@angular/forms"; import {Person} from "../../../_models/metadata/person"; import { NgbActiveModal, @@ -14,14 +22,16 @@ import { import {PersonService} from "../../../_services/person.service"; import {translate, TranslocoDirective} from '@jsverse/transloco'; import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component"; -import {forkJoin} from "rxjs"; +import {forkJoin, map, of} from "rxjs"; import {UploadService} from "../../../_services/upload.service"; import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component"; import {AccountService} from "../../../_services/account.service"; import {ToastrService} from "ngx-toastr"; +import {EditListComponent} from "../../../shared/edit-list/edit-list.component"; enum TabID { General = 'general-tab', + Aliases = 'aliases-tab', CoverImage = 'cover-image-tab', } @@ -37,7 +47,8 @@ enum TabID { NgbNavOutlet, CoverImageChooserComponent, SettingItemComponent, - NgbNavLink + NgbNavLink, + EditListComponent ], templateUrl: './edit-person-modal.component.html', styleUrl: './edit-person-modal.component.scss', @@ -117,6 +128,7 @@ export class EditPersonModalComponent implements OnInit { // @ts-ignore malId: this.editForm.get('malId')!.value === '' ? null : parseInt(this.editForm.get('malId').value, 10), hardcoverId: this.editForm.get('hardcoverId')!.value || '', + aliases: this.person.aliases, }; apis.push(this.personService.updatePerson(person)); @@ -165,4 +177,21 @@ export class EditPersonModalComponent implements OnInit { }); } + aliasValidator(): AsyncValidatorFn { + return (control: AbstractControl) => { + const name = control.value; + if (!name || name.trim().length === 0) { + return of(null); + } + + return this.personService.isValidAlias(this.person.id, name).pipe(map(valid => { + if (valid) { + return null; + } + + return { 'invalidAlias': {'alias': name} } as ValidationErrors; + })); + } + } + } diff --git a/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.html b/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.html new file mode 100644 index 000000000..2f4cb8b42 --- /dev/null +++ b/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.html @@ -0,0 +1,65 @@ + + + + + + + + diff --git a/UI/Web/src/app/admin/manage-logs/manage-logs.component.scss b/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.scss similarity index 100% rename from UI/Web/src/app/admin/manage-logs/manage-logs.component.scss rename to UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.scss diff --git a/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.ts b/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.ts new file mode 100644 index 000000000..865db0590 --- /dev/null +++ b/UI/Web/src/app/person-detail/_modal/merge-person-modal/merge-person-modal.component.ts @@ -0,0 +1,101 @@ +import {Component, DestroyRef, EventEmitter, inject, Input, OnInit} from '@angular/core'; +import {Person} from "../../../_models/metadata/person"; +import {PersonService} from "../../../_services/person.service"; +import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; +import {ToastrService} from "ngx-toastr"; +import {TranslocoDirective} from "@jsverse/transloco"; +import {TypeaheadComponent} from "../../../typeahead/_components/typeahead.component"; +import {TypeaheadSettings} from "../../../typeahead/_models/typeahead-settings"; +import {map} from "rxjs/operators"; +import {UtilityService} from "../../../shared/_services/utility.service"; +import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component"; +import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component"; +import {FilterField} from "../../../_models/metadata/v2/filter-field"; +import {Observable, of} from "rxjs"; +import {Series} from "../../../_models/series"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {AsyncPipe} from "@angular/common"; + +@Component({ + selector: 'app-merge-person-modal', + imports: [ + TranslocoDirective, + TypeaheadComponent, + SettingItemComponent, + BadgeExpanderComponent, + AsyncPipe + ], + templateUrl: './merge-person-modal.component.html', + styleUrl: './merge-person-modal.component.scss' +}) +export class MergePersonModalComponent implements OnInit { + + private readonly personService = inject(PersonService); + public readonly utilityService = inject(UtilityService); + private readonly destroyRef = inject(DestroyRef); + private readonly modal = inject(NgbActiveModal); + protected readonly toastr = inject(ToastrService); + + typeAheadSettings!: TypeaheadSettings; + typeAheadUnfocus = new EventEmitter(); + + @Input({required: true}) person!: Person; + + mergee: Person | null = null; + knownFor$: Observable | null = null; + + save() { + if (!this.mergee) { + this.close(); + return; + } + + this.personService.mergePerson(this.person.id, this.mergee.id).subscribe(person => { + this.modal.close({success: true, person: person}); + }) + } + + close() { + this.modal.close({success: false, person: this.person}); + } + + ngOnInit(): void { + this.typeAheadSettings = new TypeaheadSettings(); + this.typeAheadSettings.minCharacters = 0; + this.typeAheadSettings.multiple = false; + this.typeAheadSettings.addIfNonExisting = false; + this.typeAheadSettings.id = "merge-person-modal-typeahead"; + this.typeAheadSettings.compareFn = (options: Person[], filter: string) => { + return options.filter(m => this.utilityService.filter(m.name, filter)); + } + this.typeAheadSettings.selectionCompareFn = (a: Person, b: Person) => { + return a.name == b.name; + } + this.typeAheadSettings.fetchFn = (filter: string) => { + if (filter.length == 0) return of([]); + + return this.personService.searchPerson(filter).pipe(map(people => { + return people.filter(p => this.utilityService.filter(p.name, filter) && p.id != this.person.id); + })); + }; + + this.typeAheadSettings.trackByIdentityFn = (index, value) => `${value.name}_${value.id}`; + } + + updatePerson(people: Person[]) { + if (people.length == 0) return; + + this.typeAheadUnfocus.emit(this.typeAheadSettings.id); + this.mergee = people[0]; + this.knownFor$ = this.personService.getSeriesMostKnownFor(this.mergee.id) + .pipe(takeUntilDestroyed(this.destroyRef)); + } + + protected readonly FilterField = FilterField; + + allNewAliases() { + if (!this.mergee) return []; + + return [this.mergee.name, ...this.mergee.aliases] + } +} diff --git a/UI/Web/src/app/person-detail/person-detail.component.html b/UI/Web/src/app/person-detail/person-detail.component.html index 507521642..7952b24a3 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.html +++ b/UI/Web/src/app/person-detail/person-detail.component.html @@ -5,7 +5,7 @@

    - + {{person.name}} @if (person.aniListId) { @@ -43,15 +43,43 @@
    - + + + + @if (person.aliases.length > 0) { + {{t('aka-title')}} +
    + + + {{item}} + + +
    + } + @if (roles$ | async; as roles) { -
    -
    {{t('all-roles')}}
    - @for(role of roles; track role) { - {{role | personRole}} - } -
    + @if (roles.length > 0) { + {{t('all-roles')}} +
    + + + {{item | personRole}} + + +
    + } + + + + + + + + + }
    diff --git a/UI/Web/src/app/person-detail/person-detail.component.ts b/UI/Web/src/app/person-detail/person-detail.component.ts index e2dc192cb..31b7f976b 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.ts +++ b/UI/Web/src/app/person-detail/person-detail.component.ts @@ -1,31 +1,30 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, DestroyRef, + Component, + DestroyRef, ElementRef, - Inject, - inject, OnInit, + inject, + OnInit, ViewChild } from '@angular/core'; import {ActivatedRoute, Router} from "@angular/router"; import {PersonService} from "../_services/person.service"; import {BehaviorSubject, EMPTY, Observable, switchMap, tap} from "rxjs"; import {Person, PersonRole} from "../_models/metadata/person"; -import {AsyncPipe, NgStyle} from "@angular/common"; +import {AsyncPipe} from "@angular/common"; import {ImageComponent} from "../shared/image/image.component"; import {ImageService} from "../_services/image.service"; import { SideNavCompanionBarComponent } from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component"; import {ReadMoreComponent} from "../shared/read-more/read-more.component"; -import {TagBadgeComponent, TagBadgeCursor} from "../shared/tag-badge/tag-badge.component"; import {PersonRolePipe} from "../_pipes/person-role.pipe"; import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component"; -import {SeriesCardComponent} from "../cards/series-card/series-card.component"; import {FilterComparison} from "../_models/metadata/v2/filter-comparison"; import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service"; import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2"; -import {allPeople, personRoleForFilterField} from "../_models/metadata/v2/filter-field"; +import {allPeople, FilterField, personRoleForFilterField} from "../_models/metadata/v2/filter-field"; import {Series} from "../_models/series"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {FilterCombination} from "../_models/metadata/v2/filter-combination"; @@ -42,28 +41,38 @@ import {DefaultModalOptions} from "../_models/default-modal-options"; import {ToastrService} from "ngx-toastr"; import {LicenseService} from "../_services/license.service"; import {SafeUrlPipe} from "../_pipes/safe-url.pipe"; +import {MergePersonModalComponent} from "./_modal/merge-person-modal/merge-person-modal.component"; +import {EVENTS, MessageHubService} from "../_services/message-hub.service"; +import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.component"; + +interface PersonMergeEvent { + srcId: number, + dstId: number, + dstName: number, +} + @Component({ - selector: 'app-person-detail', - imports: [ - AsyncPipe, - ImageComponent, - SideNavCompanionBarComponent, - ReadMoreComponent, - TagBadgeComponent, - PersonRolePipe, - CarouselReelComponent, - CardItemComponent, - CardActionablesComponent, - TranslocoDirective, - ChapterCardComponent, - SafeUrlPipe - ], - templateUrl: './person-detail.component.html', - styleUrl: './person-detail.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-person-detail', + imports: [ + AsyncPipe, + ImageComponent, + SideNavCompanionBarComponent, + ReadMoreComponent, + PersonRolePipe, + CarouselReelComponent, + CardItemComponent, + CardActionablesComponent, + TranslocoDirective, + ChapterCardComponent, + SafeUrlPipe, + BadgeExpanderComponent + ], + templateUrl: './person-detail.component.html', + styleUrl: './person-detail.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush }) -export class PersonDetailComponent { +export class PersonDetailComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly filterUtilityService = inject(FilterUtilitiesService); @@ -77,8 +86,9 @@ export class PersonDetailComponent { protected readonly licenseService = inject(LicenseService); private readonly themeService = inject(ThemeService); private readonly toastr = inject(ToastrService); + private readonly messageHubService = inject(MessageHubService) - protected readonly TagBadgeCursor = TagBadgeCursor; + protected readonly FilterField = FilterField; @ViewChild('scrollingBlock') scrollingBlock: ElementRef | undefined; @ViewChild('companionBar') companionBar: ElementRef | undefined; @@ -88,11 +98,11 @@ export class PersonDetailComponent { roles$: Observable | null = null; roles: PersonRole[] | null = null; works$: Observable | null = null; - defaultSummaryText = 'No information about this Person'; filter: SeriesFilterV2 | null = null; personActions: Array> = this.actionService.getPersonActions(this.handleAction.bind(this)); chaptersByRole: any = {}; anilistUrl: string = ''; + private readonly personSubject = new BehaviorSubject(null); protected readonly person$ = this.personSubject.asObservable().pipe(tap(p => { if (p?.aniListId) { @@ -118,43 +128,58 @@ export class PersonDetailComponent { return this.personService.get(personName); }), tap((person) => { - if (person == null) { this.toastr.error(translate('toasts.unauthorized-1')); this.router.navigateByUrl('/home'); return; } - this.person = person; - this.personSubject.next(person); // emit the person data for subscribers - this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor); - - // Fetch roles and process them - this.roles$ = this.personService.getRolesForPerson(this.person.id).pipe( - tap(roles => { - this.roles = roles; - this.filter = this.createFilter(roles); - this.chaptersByRole = {}; // Reset chaptersByRole for each person - - // Populate chapters by role - roles.forEach(role => { - this.chaptersByRole[role] = this.personService.getChaptersByRole(person.id, role) - .pipe(takeUntilDestroyed(this.destroyRef)); - }); - this.cdRef.markForCheck(); - }), - takeUntilDestroyed(this.destroyRef) - ); - - // Fetch series known for this person - this.works$ = this.personService.getSeriesMostKnownFor(person.id).pipe( - takeUntilDestroyed(this.destroyRef) - ); + this.setPerson(person); }), takeUntilDestroyed(this.destroyRef) ).subscribe(); } + ngOnInit(): void { + this.messageHubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => { + if (message.event !== EVENTS.PersonMerged) return; + + const event = message.payload as PersonMergeEvent; + if (event.srcId !== this.person?.id) return; + + this.router.navigate(['person', event.dstName]); + }); + } + + private setPerson(person: Person) { + this.person = person; + this.personSubject.next(person); // emit the person data for subscribers + this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor); + + // Fetch roles and process them + this.roles$ = this.personService.getRolesForPerson(this.person.id).pipe( + tap(roles => { + this.roles = roles; + this.filter = this.createFilter(roles); + this.chaptersByRole = {}; // Reset chaptersByRole for each person + + // Populate chapters by role + roles.forEach(role => { + this.chaptersByRole[role] = this.personService.getChaptersByRole(person.id, role) + .pipe(takeUntilDestroyed(this.destroyRef)); + }); + this.cdRef.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) + ); + + // Fetch series known for this person + this.works$ = this.personService.getSeriesMostKnownFor(person.id).pipe( + takeUntilDestroyed(this.destroyRef) + ); + + } + createFilter(roles: PersonRole[]) { const filter: SeriesFilterV2 = this.filterUtilityService.createSeriesV2Filter(); filter.combination = FilterCombination.Or; @@ -229,14 +254,27 @@ export class PersonDetailComponent { } }); break; + case (Action.Merge): + this.mergePersonAction(); + break; default: break; } } - performAction(action: ActionItem) { - if (typeof action.callback === 'function') { - action.callback(action, this.person); - } + private mergePersonAction() { + const ref = this.modalService.open(MergePersonModalComponent, DefaultModalOptions); + ref.componentInstance.person = this.person; + + ref.closed.subscribe(r => { + if (r.success) { + // Reload the person data, as relations may have changed + this.personService.get(r.person.name).subscribe(person => { + this.setPerson(person!); + this.cdRef.markForCheck(); + }) + } + }); } + } diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index e03368df3..9f45cd55a 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -10,12 +10,10 @@

    - {{readingList.title}} - @if (readingList.promoted) { - () - } - - @if( isLoading) { + + + {{readingList.title}} + @if(isLoading) {
    loading...
    @@ -87,7 +85,7 @@
    - +
    diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 64a933552..511811fe8 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -273,11 +273,6 @@ export class ReadingListDetailComponent implements OnInit { }); } - performAction(action: ActionItem) { - if (typeof action.callback === 'function') { - action.callback(action, this.readingList); - } - } readChapter(item: ReadingListItem) { if (!this.readingList) return; @@ -387,12 +382,6 @@ export class ReadingListDetailComponent implements OnInit { {queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}}); } - updateAccessibilityMode() { - this.accessibilityMode = !this.accessibilityMode; - this.cdRef.markForCheck(); - } - - toggleReorder() { this.formGroup.get('edit')?.setValue(!this.formGroup.get('edit')!.value); this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html index 3d1192600..dd7dcab9a 100644 --- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html @@ -3,7 +3,7 @@

    {{t('title')}} - +

    @if (pagination) {
    {{t('item-count', {num: pagination.totalItems | number})}}
    diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index ea08f2262..9587eb026 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -22,7 +22,7 @@

    - @if (series.localizedName !== series.name) { + @if (series.localizedName !== series.name && series.localizedName) { {{series.localizedName | defaultValue}} }
    @@ -96,7 +96,7 @@
    - +
    @@ -130,7 +130,7 @@ {{t('publication-status-title')}}
    @if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) { - {{pubStatus}} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss index 26ef0aabc..158f2ce01 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.scss @@ -30,3 +30,7 @@ :host ::ng-deep .card-actions.btn-actions .btn { padding: 0.375rem 0.75rem; } + +.font-size { + font-size: 0.8rem; +} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 1fd9007dc..05580bed0 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -61,11 +61,6 @@ import {ReaderService} from 'src/app/_services/reader.service'; import {ReadingListService} from 'src/app/_services/reading-list.service'; import {ScrollService} from 'src/app/_services/scroll.service'; import {SeriesService} from 'src/app/_services/series.service'; -import { - ReviewModalCloseAction, - ReviewModalCloseEvent, - ReviewModalComponent -} from '../../../_single-module/review-modal/review-modal.component'; import {PageLayoutMode} from 'src/app/_models/page-layout-mode'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {UserReview} from "../../../_single-module/review-card/user-review"; @@ -73,8 +68,6 @@ import {ExternalSeriesCardComponent} from '../../../cards/external-series-card/e import {SeriesCardComponent} from '../../../cards/series-card/series-card.component'; import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller'; import {BulkOperationsComponent} from '../../../cards/bulk-operations/bulk-operations.component'; -import {ReviewCardComponent} from '../../../_single-module/review-card/review-card.component'; -import {CarouselReelComponent} from '../../../carousel/_components/carousel-reel/carousel-reel.component'; import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco"; import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component"; import {PublicationStatus} from "../../../_models/metadata/publication-status"; @@ -1138,13 +1131,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { }); } - - performAction(action: ActionItem) { - if (typeof action.callback === 'function') { - action.callback(action, this.series); - } - } - downloadSeries() { this.downloadService.download('series', this.series, (d) => { this.downloadInProgress = !!d; diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 94c5cf696..afb63ab1d 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -1,12 +1,12 @@ -import { HttpParams } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { Chapter } from 'src/app/_models/chapter'; -import { LibraryType } from 'src/app/_models/library/library'; -import { MangaFormat } from 'src/app/_models/manga-format'; -import { PaginatedResult } from 'src/app/_models/pagination'; -import { Series } from 'src/app/_models/series'; -import { Volume } from 'src/app/_models/volume'; -import {translate, TranslocoService} from "@jsverse/transloco"; +import {HttpParams} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Chapter} from 'src/app/_models/chapter'; +import {LibraryType} from 'src/app/_models/library/library'; +import {MangaFormat} from 'src/app/_models/manga-format'; +import {PaginatedResult} from 'src/app/_models/pagination'; +import {Series} from 'src/app/_models/series'; +import {Volume} from 'src/app/_models/volume'; +import {translate} from "@jsverse/transloco"; import {debounceTime, ReplaySubject, shareReplay} from "rxjs"; export enum KEY_CODES { @@ -21,6 +21,7 @@ export enum KEY_CODES { B = 'b', F = 'f', H = 'h', + K = 'k', BACKSPACE = 'Backspace', DELETE = 'Delete', SHIFT = 'Shift' @@ -41,6 +42,9 @@ export class UtilityService { public readonly activeBreakpointSource = new ReplaySubject(1); public readonly activeBreakpoint$ = this.activeBreakpointSource.asObservable().pipe(debounceTime(60), shareReplay({bufferSize: 1, refCount: true})); + // TODO: I need an isPhone/Tablet so that I can easily trigger different views + + mangaFormatKeys: string[] = []; diff --git a/UI/Web/src/app/shared/badge-expander/badge-expander.component.scss b/UI/Web/src/app/shared/badge-expander/badge-expander.component.scss index cf2445645..342ab4431 100644 --- a/UI/Web/src/app/shared/badge-expander/badge-expander.component.scss +++ b/UI/Web/src/app/shared/badge-expander/badge-expander.component.scss @@ -5,4 +5,11 @@ .collapsed { height: 35px; overflow: hidden; -} \ No newline at end of file +} + +::ng-deep .badge-expander .content { + a, + span { + font-size: 0.8rem; + } +} diff --git a/UI/Web/src/app/shared/edit-list/edit-list.component.html b/UI/Web/src/app/shared/edit-list/edit-list.component.html index 0231252d6..930f9720a 100644 --- a/UI/Web/src/app/shared/edit-list/edit-list.component.html +++ b/UI/Web/src/app/shared/edit-list/edit-list.component.html @@ -1,6 +1,7 @@
    - @for(item of ItemsArray.controls; let i = $index; track i) { + + @for(item of ItemsArray.controls; let i = $index; track item; let last = $last) {
    @@ -11,21 +12,30 @@ [formControlName]="i" id="item--{{i}}" > + @if (item.dirty && item.touched && errorMessage) { + @if (item.status === "INVALID") { +
    + {{errorMessage}} +
    + } + }
    -
    - +
    + @if (last){ + + }
    } diff --git a/UI/Web/src/app/shared/edit-list/edit-list.component.ts b/UI/Web/src/app/shared/edit-list/edit-list.component.ts index 6d21549e8..c5b3d121e 100644 --- a/UI/Web/src/app/shared/edit-list/edit-list.component.ts +++ b/UI/Web/src/app/shared/edit-list/edit-list.component.ts @@ -9,7 +9,7 @@ import { OnInit, Output } from '@angular/core'; -import {FormArray, FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {AsyncValidatorFn, FormArray, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn} from "@angular/forms"; import {TranslocoDirective} from "@jsverse/transloco"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators"; @@ -28,6 +28,10 @@ export class EditListComponent implements OnInit { @Input({required: true}) items: Array = []; @Input({required: true}) label = ''; + @Input() validators: ValidatorFn[] = [] + @Input() asyncValidators: AsyncValidatorFn[] = []; + // TODO: Make this more dynamic based on which validator failed + @Input() errorMessage: string | null = null; @Output() updateItems = new EventEmitter>(); form: FormGroup = new FormGroup({items: new FormArray([])}); @@ -39,6 +43,9 @@ export class EditListComponent implements OnInit { ngOnInit() { this.items.forEach(item => this.addItem(item)); + if (this.items.length === 0) { + this.addItem(""); + } this.form.valueChanges.pipe( @@ -51,7 +58,7 @@ export class EditListComponent implements OnInit { } createItemControl(value: string = ''): FormControl { - return new FormControl(value, []); + return new FormControl(value, this.validators, this.asyncValidators); } add() { @@ -69,6 +76,7 @@ export class EditListComponent implements OnInit { if (this.ItemsArray.length === 1) { this.ItemsArray.at(0).setValue(''); this.emit(); + this.cdRef.markForCheck(); return; } diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html index 3e178859c..a20571b91 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.html @@ -8,8 +8,7 @@ - + @@ -44,8 +43,7 @@ [imageUrl]="getLibraryImage(navStream.library!)" [title]="navStream.library!.name" [comparisonMethod]="'startsWith'"> - + } diff --git a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts index 9ac7df15c..547da378e 100644 --- a/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/_components/side-nav/side-nav.component.ts @@ -155,24 +155,25 @@ export class SideNavComponent implements OnInit { } async handleAction(action: ActionItem, library: Library) { + const lib = library; switch (action.action) { case(Action.Scan): - await this.actionService.scanLibrary(library); + await this.actionService.scanLibrary(lib); break; case(Action.RefreshMetadata): - await this.actionService.refreshLibraryMetadata(library); + await this.actionService.refreshLibraryMetadata(lib); break; case(Action.GenerateColorScape): - await this.actionService.refreshLibraryMetadata(library, undefined, false); + await this.actionService.refreshLibraryMetadata(lib, undefined, false); break; case (Action.AnalyzeFiles): - await this.actionService.analyzeFiles(library); + await this.actionService.analyzeFiles(lib); break; case (Action.Delete): - await this.actionService.deleteLibrary(library); + await this.actionService.deleteLibrary(lib); break; case (Action.Edit): - this.actionService.editLibrary(library, () => window.scrollTo(0, 0)); + this.actionService.editLibrary(lib, () => window.scrollTo(0, 0)); break; default: break; @@ -191,6 +192,7 @@ export class SideNavComponent implements OnInit { performAction(action: ActionItem, library: Library) { if (typeof action.callback === 'function') { + console.log('library: ', library) action.callback(action, library); } } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index d8a0ff752..2333dee25 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -130,7 +130,8 @@ export class LibrarySettingsModalComponent implements OnInit { get IsMetadataDownloadEligible() { const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType; - return libType === LibraryType.Manga || libType === LibraryType.LightNovel || libType === LibraryType.ComicVine; + return libType === LibraryType.Manga || libType === LibraryType.LightNovel + || libType === LibraryType.ComicVine || libType === LibraryType.Comic; } ngOnInit(): void { @@ -256,12 +257,12 @@ export class LibrarySettingsModalComponent implements OnInit { // TODO: Refactor into FormArray for(let fileTypeGroup of allFileTypeGroup) { - this.libraryForm.addControl(fileTypeGroup + '', new FormControl(this.library.libraryFileTypes.includes(fileTypeGroup), [])); + this.libraryForm.addControl(fileTypeGroup + '', new FormControl((this.library.libraryFileTypes || []).includes(fileTypeGroup), [])); } // TODO: Refactor into FormArray for(let glob of this.library.excludePatterns) { - this.libraryForm.addControl('excludeGlob-' , new FormControl(glob, [])); + this.libraryForm.addControl('excludeGlob-', new FormControl(glob, [])); } this.excludePatterns = this.library.excludePatterns; diff --git a/UI/Web/src/app/typeahead/_components/typeahead.component.ts b/UI/Web/src/app/typeahead/_components/typeahead.component.ts index 223676b3a..17dbc7b4c 100644 --- a/UI/Web/src/app/typeahead/_components/typeahead.component.ts +++ b/UI/Web/src/app/typeahead/_components/typeahead.component.ts @@ -72,6 +72,10 @@ export class TypeaheadComponent implements OnInit { * When triggered, will focus the input if the passed string matches the id */ @Input() focus: EventEmitter | undefined; + /** + * When triggered, will unfocus the input if the passed string matches the id + */ + @Input() unFocus: EventEmitter | undefined; @Output() selectedData = new EventEmitter(); @Output() newItemAdded = new EventEmitter(); // eslint-disable-next-line @angular-eslint/no-output-on-prefix @@ -113,6 +117,13 @@ export class TypeaheadComponent implements OnInit { }); } + if (this.unFocus) { + this.unFocus.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((id: string) => { + if (this.settings.id !== id) return; + this.hasFocus = false; + }); + } + this.init(); } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index 3e00f89aa..8ef4f814c 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -77,7 +77,7 @@
    - +
    diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.ts b/UI/Web/src/app/volume-detail/volume-detail.component.ts index 838925030..7460bdcab 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -80,6 +80,7 @@ import {UserReview} from "../_single-module/review-card/user-review"; import {ReviewsComponent} from "../_single-module/reviews/reviews.component"; import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component"; import {ChapterService} from "../_services/chapter.service"; +import {User} from "../_models/user"; enum TabID { @@ -187,6 +188,7 @@ export class VolumeDetailComponent implements OnInit { protected readonly TabID = TabID; protected readonly FilterField = FilterField; protected readonly Breakpoint = Breakpoint; + protected readonly encodeURIComponent = encodeURIComponent; @ViewChild('scrollingBlock') scrollingBlock: ElementRef | undefined; @ViewChild('companionBar') companionBar: ElementRef | undefined; @@ -211,7 +213,7 @@ export class VolumeDetailComponent implements OnInit { mobileSeriesImgBackground: string | undefined; downloadInProgress: boolean = false; - volumeActions: Array> = this.actionFactoryService.getVolumeActions(this.handleVolumeAction.bind(this)); + volumeActions: Array> = this.actionFactoryService.getVolumeActions(this.handleVolumeAction.bind(this), this.shouldRenderVolumeAction.bind(this)); chapterActions: Array> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)); bulkActionCallback = async (action: ActionItem, _: any) => { @@ -570,16 +572,6 @@ export class VolumeDetailComponent implements OnInit { this.location.replaceState(newUrl) } - openPerson(field: FilterField, value: number) { - this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe(); - } - - performAction(action: ActionItem) { - if (typeof action.callback === 'function') { - action.callback(action, this.volume!); - } - } - async handleChapterActionCallback(action: ActionItem, chapter: Chapter) { switch (action.action) { case(Action.MarkAsRead): @@ -610,6 +602,17 @@ export class VolumeDetailComponent implements OnInit { } } + shouldRenderVolumeAction(action: ActionItem, entity: Volume, user: User) { + switch (action.action) { + case(Action.MarkAsRead): + return entity.pagesRead < entity.pages; + case(Action.MarkAsUnread): + return entity.pagesRead !== 0; + default: + return true; + } + } + async handleVolumeAction(action: ActionItem) { switch (action.action) { case Action.Delete: @@ -687,6 +690,4 @@ export class VolumeDetailComponent implements OnInit { this.currentlyReadingChapter = undefined; } } - - protected readonly encodeURIComponent = encodeURIComponent; } diff --git a/UI/Web/src/assets/langs/cs.json b/UI/Web/src/assets/langs/cs.json index 916d23333..dfa2db0fc 100644 --- a/UI/Web/src/assets/langs/cs.json +++ b/UI/Web/src/assets/langs/cs.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, kliknutím zobrazíte" }, - "review-series-modal": { - "title": "Upravit recenzi", - "review-label": "Recenze", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Recenze musí mít alespoň {{count}} znaků", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}'s Recenze", @@ -81,7 +72,8 @@ "your-review": "Toto je vaše recenze", "external-review": "Externí recenze", "local-review": "Místní recenze", - "rating-percentage": "Hodnocení {{r}} %" + "rating-percentage": "Hodnocení {{r}} %", + "critic": "kritik" }, "want-to-read": { "title": "Chci číst", @@ -785,9 +777,7 @@ "publication-status-tooltip": "Stav publikace", "release-date-title": "Vydání", "time-left-alt": "Zbývající čas", - "user-reviews-local": "Místní hodnocení", "publication-status-title": "Publikace", - "user-reviews-plus": "Externí hodnocení", "weblinks-title": "Odkazy", "more-alt": "Více", "pages-count": "{{num}} stran", @@ -821,7 +811,8 @@ "close": "{{common.close}}", "kavita-rating-title": "Vaše hodnocení", "entry-label": "Zobrazit podrobnosti", - "kavita-tooltip": "Vaše hodnocení + celkové" + "kavita-tooltip": "Vaše hodnocení + celkové", + "critic": "{{review-card.critic}}" }, "badge-expander": { "more-items": "a {{počet}} více" @@ -1460,7 +1451,8 @@ "skip-alt": "Přeskočit na hlavní obsah", "server-settings": "Nastavení serveru", "settings": "Nastavení", - "announcements": "Oznámení" + "announcements": "Oznámení", + "person-aka-status": "Shoduje se s aliasem" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -2009,7 +2001,10 @@ "new-collection": "Nová sbírka", "match": "Shoda", "match-tooltip": "Přiřadit série s Kavitou+ manuálně", - "reorder": "Přeuspořádat" + "reorder": "Přeuspořádat", + "rename": "Přejmenovat", + "rename-tooltip": "Přejmenovat inteligentní filtr", + "merge": "Sloučit" }, "changelog-update-item": { "changed": "Změněno", @@ -2045,7 +2040,9 @@ "known-for-title": "Známý díky", "individual-role-title": "Jako {{role}}", "browse-person-title": "Všechny práce {{name}}", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "no-info": "Žádné informace o této osobě", + "aka-title": "Známé také jako " }, "draggable-ordered-list": { "reorder-label": "Přeuspořádat", @@ -2064,7 +2061,7 @@ }, "match-series-modal": { "no-results": "Nelze najít shodu.Zkuste přidat url od podporovaného poskytovatele a zkuste to znovu.", - "query-tooltip": "Zadejte název série, url AniListu/MyAnimeListu. Url budou používat přímé vyhledávání.", + "query-tooltip": "Zadejte název série, AniList/MyAnimeList/ComicBookRoundup url. Url budou používat přímé vyhledávání.", "title": "Shoda {{seriesName}}", "close": "{{common.close}}", "save": "{{common.save}}", @@ -2131,7 +2128,8 @@ "chapter-count": "{{common.chapter-count}}", "releasing": "Vychází", "details": "Zobrazit stránku", - "updating-metadata-status": "Aktualizace metadat" + "updating-metadata-status": "Aktualizace metadat", + "issue-count": "{{common.issue-count}}" }, "pdf-reader": { "incognito-mode": "Anonymní režim", @@ -2179,7 +2177,11 @@ "cover-image-tab": "{{edit-series-modal.cover-image-tab}}", "close": "{{common.close}}", "anilist-id-label": "AniList ID", - "anilist-tooltip": "https://anilist.co/staff/{AniListId}/" + "anilist-tooltip": "https://anilist.co/staff/{AniListId}/", + "aliases-tab": "Aliasy", + "aliases-label": "Upravit aliasy", + "alias-overlap": "Tento alias již odkazuje na jinou osobu nebo je jménem této osoby, zvažte jejich sloučení.", + "aliases-tooltip": "Pokud je série označena aliasem osoby, je tato osoba přiřazena, místo aby byla vytvořena nová osoba. Při odstranění aliasu je nutné sérii znovu prohledat, aby se změna zachytila." }, "errors": { "delete-theme-in-use": "Motiv je aktuálně používán alespoň jedním uživatelem, nelze odstranit", @@ -2458,7 +2460,8 @@ "match-success": "Správně sladěné série", "webtoon-override": "Přepnutí do režimu Webtoon kvůli obrázkům představujícím webtoon.", "scrobble-gen-init": "Vytvořena úloha pro generování událostí scrobble z historie čtení, hodnocení v minulosti a jejich synchronizaci s připojenými službami.", - "confirm-delete-multiple-volumes": "Jste si jisti, že chcete odstranit {{count}} svazků? Soubory na disku se tím nezmění." + "confirm-delete-multiple-volumes": "Jste si jisti, že chcete odstranit {{count}} svazků? Soubory na disku se tím nezmění.", + "series-added-want-to-read": "Série přidána ze seznamu Chci číst" }, "preferences": { "split-right-to-left": "Rozdělit zprava doleva", @@ -2620,7 +2623,18 @@ "localized-name-tooltip": "Povolit zápis lokalizovaného názvu při odemknutí pole. Kavita se pokusí provést nejlepší odhad.", "enable-cover-image-label": "Obrázek na obálce", "overrides-description": "Povolit Kavitě zápis přes uzamčená pole.", - "overrides-label": "Přepisuje" + "overrides-label": "Přepisuje", + "enable-chapter-release-date-label": "Datum vydání", + "enable-chapter-publisher-label": "Vydavatel", + "enable-chapter-publisher-tooltip": "Umožnit napsání vydavatele kapitoly/vydání", + "enable-chapter-cover-label": "Obálka kapitoly", + "enable-chapter-cover-tooltip": "Umožnit nastavení obálky kapitoly/vydání", + "chapter-header": "Pole kapitoly", + "enable-chapter-release-date-tooltip": "Povolit datum vydání kapitoly/vydání, které má být napsáno", + "enable-chapter-title-label": "Titulek", + "enable-chapter-title-tooltip": "Umožnit napsání názvu kapitoly/vydání", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}" }, "metadata-setting-field-pipe": { "people": "{{tabs.people-tab}}", @@ -2631,7 +2645,12 @@ "covers": "Obálky", "genres": "{{metadata-fields.genres-title}}", "summary": "{{filter-field-pipe.summary}}", - "publication-status": "{{edit-series-modal.publication-status-title}}" + "publication-status": "{{edit-series-modal.publication-status-title}}", + "chapter-title": "Název (kapitola)", + "chapter-covers": "Obálky (kapitola)", + "chapter-release-date": "Datum vydání (kapitola)", + "chapter-summary": "Shrnutí (kapitola)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Kapitola)" }, "role-localized-pipe": { "download": "Stažené", @@ -2649,5 +2668,27 @@ "trace": "Sledovat", "warning": "Varování", "critical": "Kritické" + }, + "review-modal": { + "title": "Upravit recenzi", + "min-length": "Recenze musí mít alespoň {{count}} znaků", + "required": "{{validation.required-field}}", + "delete": "{{common.delete}}", + "save": "{{common.save}}", + "review-label": "Recenze", + "close": "{{common.close}}" + }, + "reviews": { + "user-reviews-local": "Místní recenze", + "user-reviews-plus": "Externí recenze" + }, + "merge-person-modal": { + "title": "{{personName}}", + "close": "{{common.close}}", + "save": "{{common.save}}", + "src": "Sloučit osoby", + "merge-warning": "Pokud budete pokračovat, vybraná osoba bude odstraněna. Jméno vybrané osoby bude přidáno jako alias a všechny její role budou převedeny.", + "alias-title": "Nové aliasy", + "known-for-title": "Známý pro" } } diff --git a/UI/Web/src/assets/langs/da.json b/UI/Web/src/assets/langs/da.json index 1ecb88f36..ac0ed2c76 100644 --- a/UI/Web/src/assets/langs/da.json +++ b/UI/Web/src/assets/langs/da.json @@ -16,12 +16,6 @@ "filter-label": "{{common.filter}}", "special": "{{entity-title.special}}" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}" }, diff --git a/UI/Web/src/assets/langs/de.json b/UI/Web/src/assets/langs/de.json index 65b44dbf9..376e33a7e 100644 --- a/UI/Web/src/assets/langs/de.json +++ b/UI/Web/src/assets/langs/de.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, klicke zum Anzeigen" }, - "review-series-modal": { - "title": "Rezension bearbeiten", - "review-label": "Rezension", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Rezension muss mindestens {{count}} Zeichen lang sein", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}'s Rezension", @@ -762,8 +753,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "Zu diesem Band gibt es keine Kapitel. Kann nicht gelesen werden.", "cover-change": "Es kann bis zu einer Minute dauern, bis Ihr Browser das Bild aktualisiert hat. Bis dahin kann auf einigen Seiten noch das alte Bild angezeigt werden.", - "user-reviews-local": "Lokale Bewertungen", - "user-reviews-plus": "Externe Bewertungen", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/el.json b/UI/Web/src/assets/langs/el.json index ef5c92df3..abae28c97 100644 --- a/UI/Web/src/assets/langs/el.json +++ b/UI/Web/src/assets/langs/el.json @@ -55,15 +55,6 @@ "spoiler": { "click-to-show": "Spoiler, κάντε κλικ για να το δείτε" }, - "review-series-modal": { - "title": "Επεξεργασία Κριτικής", - "review-label": "Κριτική", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Η κριτική πρέπει να περιέχει τουλάχιστον {{count}} χαρακτήρες", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Η κριτική του {{username}}", @@ -290,8 +281,6 @@ "info-tab": "{{tabs.info-tab}}", "recommendations-tab": "{{tabs.recommendations-tab}}", "no-pages": "{{toasts.no-pages}}", - "user-reviews-local": "Τοπικές Κριτικές", - "user-reviews-plus": "Εξωτερικές Κριτικές", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 08ac0515c..19d4443b6 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1003,7 +1003,7 @@ "save": "{{common.save}}", "no-results": "Unable to find a match. Try adding the url from a supported provider and retry.", "query-label": "Query", - "query-tooltip": "Enter series name, AniList/MyAnimeList url. Urls will use a direct lookup.", + "query-tooltip": "Enter series name, AniList/MyAnimeList/ComicBookRoundup url. Urls will use a direct lookup.", "dont-match-label": "Do not Match", "dont-match-tooltip": "Opt this series from matching and scrobbling", "search": "Search" @@ -1103,12 +1103,14 @@ }, "person-detail": { + "aka-title": "Also known as ", "known-for-title": "Known For", "individual-role-title": "As a {{role}}", "browse-person-title": "All Works of {{name}}", "browse-person-by-role-title": "All Works of {{name}} as a {{role}}", "all-roles": "Roles", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "no-info": "No information about this Person" }, "library-settings-modal": { @@ -1857,7 +1859,8 @@ "logout": "Logout", "all-filters": "Smart Filters", "nav-link-header": "Navigation Options", - "close": "{{common.close}}" + "close": "{{common.close}}", + "person-aka-status": "Matches an alias" }, "promoted-icon": { @@ -2246,6 +2249,7 @@ "title": "{{personName}} Details", "general-tab": "{{edit-series-modal.general-tab}}", "cover-image-tab": "{{edit-series-modal.cover-image-tab}}", + "aliases-tab": "Aliases", "loading": "{{common.loading}}", "close": "{{common.close}}", "name-label": "{{edit-series-modal.name-label}}", @@ -2263,7 +2267,20 @@ "cover-image-description": "{{edit-series-modal.cover-image-description}}", "cover-image-description-extra": "Alternatively you can download a cover from CoversDB if available.", "save": "{{common.save}}", - "download-coversdb": "Download from CoversDB" + "download-coversdb": "Download from CoversDB", + "aliases-label": "Edit aliases", + "alias-overlap": "This alias already points towards another person or is the name of this person, consider merging them.", + "aliases-tooltip": "When a series is tagged with an alias of a person, the person is assigned rather than creating a new person. When deleting an alias, you'll have to rescan the series for the change to be picked up." + }, + + "merge-person-modal": { + "title": "{{personName}}", + "close": "{{common.close}}", + "save": "{{common.save}}", + "src": "Merge Person", + "merge-warning": "If you proceed, the selected person will be removed. The selected person's name will be added as an alias, and all their roles will be transferred.", + "alias-title": "New aliases", + "known-for-title": "Known for" }, "day-breakdown": { @@ -2606,6 +2623,7 @@ "entity-unread": "{{name}} is now unread", "mark-read": "Marked as Read", "mark-unread": "Marked as Unread", + "series-added-want-to-read": "Series added from Want to Read list", "series-removed-want-to-read": "Series removed from Want to Read list", "series-deleted": "Series deleted", "delete-review": "Are you sure you want to delete your review?", @@ -2780,7 +2798,8 @@ "match-tooltip": "Match Series with Kavita+ manually", "reorder": "Reorder", "rename": "Rename", - "rename-tooltip": "Rename the Smart Filter" + "rename-tooltip": "Rename the Smart Filter", + "merge": "Merge" }, "preferences": { diff --git a/UI/Web/src/assets/langs/es.json b/UI/Web/src/assets/langs/es.json index c3fc6a4b6..d9f5ca04f 100644 --- a/UI/Web/src/assets/langs/es.json +++ b/UI/Web/src/assets/langs/es.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Alerta de spoiler, haz clic para mostrar" }, - "review-series-modal": { - "title": "Editar reseña", - "review-label": "Reseña", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "La reseña debe tener al menos {{count}} caracteres", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Reseña de {{username}}", @@ -757,8 +748,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "No existen capítulos en este volumen. No se puede leer.", "cover-change": "Puede tomar hasta un minuto para que tu navegador actualice la imagen. Espera hasta entonces. La imagen antigua se puede mostrar en algunas páginas.", - "user-reviews-local": "Reseñas locales", - "user-reviews-plus": "Reseñas externas", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/et.json b/UI/Web/src/assets/langs/et.json index 2f9802fae..fb5963f25 100644 --- a/UI/Web/src/assets/langs/et.json +++ b/UI/Web/src/assets/langs/et.json @@ -56,15 +56,6 @@ "spoiler": { "click-to-show": "Potentsiaalselt mittesoovitav eelinfo sisu kohta - kliki seda, et näidata" }, - "review-series-modal": { - "title": "Muuda ülevaade", - "review-label": "Ülevaade", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Ülevaade peab olema vähemalt {{count}} märki pikk", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}} ülevaade", diff --git a/UI/Web/src/assets/langs/fi.json b/UI/Web/src/assets/langs/fi.json index 9a316f77a..5d1f1393f 100644 --- a/UI/Web/src/assets/langs/fi.json +++ b/UI/Web/src/assets/langs/fi.json @@ -128,15 +128,6 @@ "spoiler": { "click-to-show": "Spoileri, napsauta näyttääksesi" }, - "review-series-modal": { - "title": "Muokkaa arvostelua", - "review-label": "Arvostelu", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Arvostelussa on oltava vähintään {{count}} merkkiä", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "external-mod": "(external)", diff --git a/UI/Web/src/assets/langs/fr.json b/UI/Web/src/assets/langs/fr.json index de9f74431..3ae26002d 100644 --- a/UI/Web/src/assets/langs/fr.json +++ b/UI/Web/src/assets/langs/fr.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Divulgâcheur, cliquez afin d'afficher" }, - "review-series-modal": { - "title": "Éditer la critique", - "review-label": "Critique", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "La critique doit comporter au moins {{count}} caractères", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Critique de {{username}}", @@ -762,8 +753,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "Il n'y a aucun chapitre dans ce volume. Il ne peut être lu.", "cover-change": "L'actualisation de l'image par votre navigateur peut prendre jusqu'à une minute. En attendant, l'ancienne image peut être affichée sur certaines pages.", - "user-reviews-local": "Commentaires locaux", - "user-reviews-plus": "Commentaires externes", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/ga.json b/UI/Web/src/assets/langs/ga.json index 25f961d57..94490ec07 100644 --- a/UI/Web/src/assets/langs/ga.json +++ b/UI/Web/src/assets/langs/ga.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, cliceáil chun a thaispeáint" }, - "review-series-modal": { - "title": "Cuir Athbhreithniú in Eagar", - "review-label": "Athbhreithniú", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Ní mór an t-athbhreithniú a bheith ar a laghad {{count}} carachtair", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Léirmheas {{username}}", @@ -81,7 +72,8 @@ "your-review": "Is é seo d'athbhreithniú", "external-review": "Athbhreithniú Seachtrach", "local-review": "Athbhreithniú Áitiúil", - "rating-percentage": "Rátáil {{r}}%" + "rating-percentage": "Rátáil {{r}}%", + "critic": "criticeoir" }, "want-to-read": { "title": "Ag iarraidh léamh", @@ -762,8 +754,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "Níl aon chaibidlí san imleabhar seo. Ní féidir léamh.", "cover-change": "Is féidir leis suas le nóiméad a thógáil le do bhrabhsálaí an íomhá a athnuachan. Go dtí sin, is féidir an tseaníomhá a thaispeáint ar roinnt leathanach.", - "user-reviews-local": "Léirmheasanna Áitiúla", - "user-reviews-plus": "Léirmheasanna Seachtracha", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", @@ -825,7 +815,8 @@ "entry-label": "Féach Sonraí", "kavita-tooltip": "Do Rátáil + Tríd is tríd", "kavita-rating-title": "Do Rátáil", - "close": "{{common.close}}" + "close": "{{common.close}}", + "critic": "{{review-card.critic}}" }, "badge-expander": { "more-items": "agus {{count}} níos mó" @@ -1522,7 +1513,8 @@ "logout": "Logáil amach", "all-filters": "Scagairí Cliste", "nav-link-header": "Roghanna Nascleanúna", - "close": "{{common.close}}" + "close": "{{common.close}}", + "person-aka-status": "Meaitseálann leasainm" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -2260,7 +2252,9 @@ "bulk-delete-libraries": "An bhfuil tú cinnte gur mhaith leat {{count}} leabharlann a scriosadh?", "match-success": "Sraith a mheaitseáil i gceart", "webtoon-override": "Ag aistriú go mód Webtoon mar gheall ar íomhánna a léiríonn webtoon.", - "scrobble-gen-init": "Cheangail post chun teagmhais scrobble a ghiniúint ó stair léitheoireachta agus rátálacha san am a chuaigh thart, agus iad á sioncronú le seirbhísí ceangailte." + "scrobble-gen-init": "Cheangail post chun teagmhais scrobble a ghiniúint ó stair léitheoireachta agus rátálacha san am a chuaigh thart, agus iad á sioncronú le seirbhísí ceangailte.", + "confirm-delete-multiple-volumes": "An bhfuil tú cinnte gur mian leat {{count}} imleabhar a scriosadh? Ní athróidh sé comhaid ar an diosca.", + "series-added-want-to-read": "Sraith curtha leis ón liosta Ar Mhaith Leat Léamh" }, "read-time-pipe": { "less-than-hour": "<1 Uair", @@ -2333,7 +2327,10 @@ "copy-settings": "Cóipeáil Socruithe Ó", "match": "Meaitseáil", "match-tooltip": "Sraith Meaitseála le Kavita+ de láimh", - "reorder": "Athordú" + "reorder": "Athordú", + "rename": "Athainmnigh", + "rename-tooltip": "Athainmnigh an Scagaire Cliste", + "merge": "Cumaisc" }, "preferences": { "left-to-right": "Clé go Deas", @@ -2472,7 +2469,9 @@ "browse-person-by-role-title": "Gach Oibre de {{name}} mar {{role}}", "individual-role-title": "Mar {{role}}", "all-roles": "Róil", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "aka-title": "Ar a dtugtar freisin ", + "no-info": "Gan aon eolas faoin Duine seo" }, "edit-person-modal": { "general-tab": "{{edit-series-modal.general-tab}}", @@ -2495,7 +2494,11 @@ "anilist-tooltip": "https://anilist.co/staff/{AniListId}/", "cover-image-description-extra": "Nó is féidir leat clúdach a íoslódáil ó CoversDB má tá sé ar fáil.", "hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}", - "download-coversdb": "Íoslódáil ó CoverDB" + "download-coversdb": "Íoslódáil ó CoverDB", + "aliases-label": "Cuir leasainmneacha in eagar", + "aliases-tab": "Leasainmneacha", + "aliases-tooltip": "Nuair a chuirtear leasainm duine ar shraith, sanntar an duine sin seachas duine nua a chruthú. Agus leasainm á scriosadh, beidh ort an tsraith a athscanadh le go dtabharfar faoi deara an t-athrú.", + "alias-overlap": "Tá an leasainm seo ag tagairt do dhuine eile cheana féin nó is é ainm an duine seo é, smaoinigh ar iad a chumasc." }, "new-version-modal": { "description": "Tá leagan nua de Kavita ar fáil. Athnuaigh le nuashonrú.", @@ -2532,7 +2535,7 @@ "title": "Meaitseáil {{ seriesName}}", "description": "Roghnaigh meaitseáil chun meiteashonraí Kavita+ a athshreangú agus imeachtaí scrobble a athghiniúint. Is féidir Don't Match a úsáid chun Kavita a shrianadh ó mheiteashonraí a mheaitseáil agus scrobadh a dhéanamh.", "close": "{{common.close}}", - "query-tooltip": "Cuir isteach ainm na sraithe, url AniList/MyAnimeList. Bainfidh URLanna úsáid as cuardach díreach.", + "query-tooltip": "Cuir isteach ainm na sraithe, url AniList/MyAnimeList/ComicBookRoundup. Úsáidfear cuardach díreach sna URLanna.", "dont-match-label": "Ná Meaitseáil", "dont-match-tooltip": "Rogha an tsraith seo ó mheaitseáil agus scrobbling", "search": "Cuardach" @@ -2569,7 +2572,8 @@ "volume-count": "{{server-stats.volume-count}}", "chapter-count": "{{common.chapter-count}}", "releasing": "Ag scaoileadh", - "updating-metadata-status": "Meiteashonraí á nuashonrú" + "updating-metadata-status": "Meiteashonraí á nuashonrú", + "issue-count": "{{common.issue-count}}" }, "manage-user-tokens": { "description": "Seans go mbeidh gá le hathnuachan a dhéanamh ó am go chéile ar úsáideoirí a bhfuil comharthaí scrobarnaí acu. Seolfaidh Kavita ríomhphost chucu go huathoibríoch má tá ríomhphost socraithe agus má tá ríomhphost bailí acu.", @@ -2619,7 +2623,18 @@ "overrides-description": "Lig do Kavita scríobh thar réimsí faoi ghlas.", "overrides-label": "Sáraíonn", "localized-name-tooltip": "Ceadaigh Ainm Logánaithe a scríobh nuair a dhíghlasáiltear an réimse. Déanfaidh Kavita iarracht an buille faoi thuairim is fearr a dhéanamh.", - "enable-cover-image-tooltip": "Lig do Kavita íomhá clúdaigh na Sraithe a scríobh" + "enable-cover-image-tooltip": "Lig do Kavita íomhá clúdaigh na Sraithe a scríobh", + "enable-chapter-publisher-tooltip": "Ceadaigh d’Fhoilsitheoir na Caibidle/na hEagráine a bheith scríofa", + "chapter-header": "Réimsí Caibidle", + "enable-chapter-title-label": "Teideal", + "enable-chapter-title-tooltip": "Ceadaigh Teideal na Caibidle/na hEagráin a scríobh", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-release-date-label": "Dáta Eisiúna", + "enable-chapter-release-date-tooltip": "Ceadaigh Dáta Eisiúna na Caibidle/na hEagráine a scríobh", + "enable-chapter-publisher-label": "Foilsitheoir", + "enable-chapter-cover-label": "Clúdach Caibidle", + "enable-chapter-cover-tooltip": "Ceadaigh Clúdach na Caibidle/na hEagráin a shocrú" }, "metadata-setting-field-pipe": { "people": "{{tabs.people-tab}}", @@ -2630,7 +2645,12 @@ "covers": "Clúdaíonn", "publication-status": "{{edit-series-modal.publication-status-title}}", "summary": "{{filter-field-pipe.summary}}", - "age-rating": "{{metadata-fields.age-rating-title}}" + "age-rating": "{{metadata-fields.age-rating-title}}", + "chapter-covers": "Clúdaigh (Caibidil)", + "chapter-release-date": "Dáta Eisiúna (Caibidil)", + "chapter-title": "Teideal (Caibidil)", + "chapter-summary": "Achoimre (Caibidil)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Caibidil)" }, "role-localized-pipe": { "admin": "Riarachán", @@ -2648,5 +2668,27 @@ "critical": "Criticiúil", "information": "Eolas", "debug": "Dífhabhtaithe" + }, + "review-modal": { + "required": "{{validation.required-field}}", + "title": "Cuir Athbhreithniú in Eagar", + "save": "{{common.save}}", + "min-length": "Ní mór don léirmheas a bheith {{count}} carachtar ar a laghad", + "delete": "{{common.delete}}", + "review-label": "Athbhreithniú", + "close": "{{common.close}}" + }, + "reviews": { + "user-reviews-local": "Léirmheasanna Áitiúla", + "user-reviews-plus": "Léirmheasanna Seachtracha" + }, + "merge-person-modal": { + "close": "{{common.close}}", + "alias-title": "Leasainmneacha nua", + "merge-warning": "Má leanann tú ar aghaidh, bainfear an duine roghnaithe. Cuirfear ainm an duine roghnaithe leis mar leasainm, agus aistreofar a róil go léir.", + "known-for-title": "Ar a dtugtar", + "src": "Cumaisc Duine", + "title": "{{personName}}", + "save": "{{common.save}}" } } diff --git a/UI/Web/src/assets/langs/hi.json b/UI/Web/src/assets/langs/hi.json index 76431d0ee..f7403ba75 100644 --- a/UI/Web/src/assets/langs/hi.json +++ b/UI/Web/src/assets/langs/hi.json @@ -17,12 +17,6 @@ "filter-label": "{{common.filter}}", "special": "{{entity-title.special}}" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}" }, diff --git a/UI/Web/src/assets/langs/hu.json b/UI/Web/src/assets/langs/hu.json index a95fff6a4..53c9632cd 100644 --- a/UI/Web/src/assets/langs/hu.json +++ b/UI/Web/src/assets/langs/hu.json @@ -56,15 +56,6 @@ "spoiler": { "click-to-show": "Spoiler, kattints a megnézéshez" }, - "review-series-modal": { - "title": "Áttekintés szerkesztése", - "review-label": "Áttekintés", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Az áttekintés legalább {{count}} karakter kell legyen", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}} áttekintése", diff --git a/UI/Web/src/assets/langs/id.json b/UI/Web/src/assets/langs/id.json index 3f3c50656..5843bf7da 100644 --- a/UI/Web/src/assets/langs/id.json +++ b/UI/Web/src/assets/langs/id.json @@ -54,15 +54,6 @@ "spoiler": { "click-to-show": "Spoiler, klik untuk tampilkan" }, - "review-series-modal": { - "title": "Ubah Ulasan", - "review-label": "Ulasan", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Ulasan harus setidaknya terdiri dari {{count}} karakter", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}" }, diff --git a/UI/Web/src/assets/langs/it.json b/UI/Web/src/assets/langs/it.json index c9e6be73c..bfee2d3c8 100644 --- a/UI/Web/src/assets/langs/it.json +++ b/UI/Web/src/assets/langs/it.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, clicca per mostrare" }, - "review-series-modal": { - "title": "Modifica recensione", - "review-label": "Recensione", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "La recensione deve avere almeno {{count}} caratteri", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Recensione di {{username}}", @@ -780,10 +771,8 @@ "reading-lists-title": "{{side-nav.reading-lists}}", "time-to-read-alt": "{{sort-field-pipe.time-to-read}}", "scrobbling-tooltip": "{{settings.scrobbling}}", - "user-reviews-plus": "Recensioni Esterne", "words-count": "{{num}} Parole", "more-alt": "Ancora", - "user-reviews-local": "Recensioni Locali", "pages-count": "{{num}} Pagine", "weblinks-title": "Link", "time-left-alt": "Tempo Rimanente", diff --git a/UI/Web/src/assets/langs/ja.json b/UI/Web/src/assets/langs/ja.json index d2c52f0ea..0aa935eee 100644 --- a/UI/Web/src/assets/langs/ja.json +++ b/UI/Web/src/assets/langs/ja.json @@ -61,15 +61,6 @@ "spoiler": { "click-to-show": "クリックするとネタバレが表示されます" }, - "review-series-modal": { - "title": "レビューを編集する", - "review-label": "レビュー", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "レビューは最低 {{count}}文字以上である必要があります。", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}さんのレビュー", @@ -726,8 +717,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "この巻にはチャプターが存在しません。読むことはできません。", "cover-change": "ブラウザが画像をリフレッシュするまで最大1分かかる場合があります。その間、一部のページでは古い画像が表示される可能性があります。", - "user-reviews-local": "ローカルレビュー", - "user-reviews-plus": "外部レビュー", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/ko.json b/UI/Web/src/assets/langs/ko.json index 4da21a4e7..449fdcae2 100644 --- a/UI/Web/src/assets/langs/ko.json +++ b/UI/Web/src/assets/langs/ko.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "스포일러, 표시하려면 클릭" }, - "review-series-modal": { - "title": "리뷰 수정", - "review-label": "평가", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "리뷰는 적어도 {{count}}자여야 합니다", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}님의 리뷰", @@ -81,7 +72,8 @@ "your-review": "당신의 리뷰", "external-review": "외부 리뷰", "local-review": "로컬 리뷰", - "rating-percentage": "평점 {{r}}%" + "rating-percentage": "평점 {{r}}%", + "critic": "평점" }, "want-to-read": { "title": "읽고 싶어요", @@ -517,9 +509,9 @@ }, "library-type-pipe": { "book": "책", - "comic": "만화(Comic)", + "comic": "만화 (Legacy)", "manga": "만화(Manga)", - "comicVine": "Comic Vine", + "comicVine": "코믹(Comic)", "image": "이미지", "lightNovel": "라이트 노벨" }, @@ -762,8 +754,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "이 볼륨에는 챕터가 없습니다. 읽을 수 없습니다.", "cover-change": "브라우저에서 이미지를 새로 고치는 데 최대 1분이 걸릴 수 있습니다. 그때까지는 일부 페이지에 이전 이미지가 표시될 수 있습니다.", - "user-reviews-local": "로컬 리뷰", - "user-reviews-plus": "외부 리뷰", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", @@ -789,7 +779,7 @@ "more-alt": "더 보기", "time-left-alt": "남은 시간", "time-to-read-alt": "{{sort-field-pipe.time-to-read}}", - "scrobbling-tooltip": "{{settings.scrobbling}}", + "scrobbling-tooltip": "{{settings.scrobbling}}: {{value}}", "publication-status-title": "출판", "publication-status-tooltip": "출판현황", "on": "{{reader-settings.on}}", @@ -825,7 +815,8 @@ "entry-label": "세부정보 보기", "kavita-tooltip": "귀하의 평가 + 전체", "kavita-rating-title": "귀하의 평가", - "close": "{{common.close}}" + "close": "{{common.close}}", + "critic": "{{review-card.critic}}" }, "badge-expander": { "more-items": "와 {{count}} 더" @@ -861,7 +852,8 @@ "more": "더 보기", "customize": "{{settings.customize}}", "browse-authors": "작가 찾아보기", - "edit": "{{common.edit}}" + "edit": "{{common.edit}}", + "cancel-edit": "재정렬 닫기" }, "library-settings-modal": { "close": "{{common.close}}", @@ -878,7 +870,7 @@ "type-label": "유형", "type-tooltip": "라이브러리 유형에 따라 파일 이름이 구문 분석되는 방식과 UI에 장(만화)과 이슈(만화)가 표시되는지 여부가 결정됩니다. 라이브러리 유형 간의 차이점에 대한 자세한 내용은 Wiki를 확인하세요.", "kavitaplus-eligible-label": "Kavita+ 적격", - "kavitaplus-eligible-tooltip": "Kavita+가 정보를 가져오거나 스크로블링을 지원합니까", + "kavitaplus-eligible-tooltip": "Kavita+ 메타데이터 기능 또는 자동 추천 지원", "folder-description": "라이브러리에 폴더 추가", "browse": "미디어 폴더 찾아보기", "help-us-part-1": "팔로우하여 저희를 도와주세요 ", @@ -1086,7 +1078,7 @@ "reset": "{{common.reset}}", "test": "테스트", "host-name-label": "호스트 이름", - "host-name-tooltip": "도메인 이름(역방향 프록시의). 이메일 기능에 필요합니다. 역방향 프록시가 없으면 임의의 URL을 사용하십시오.", + "host-name-tooltip": "이메일 기능에 필요한 역방향 프록시의 도메인 이름입니다. 역방향 프록시를 사용하지 않는 경우, http://externalip:port/ 형식 등 어떠한 URL도 사용할 수 있습니다", "host-name-validation": "호스트 이름은 http(s)로 시작하고 /로 끝나지 않아야 합니다", "sender-address-label": "송신자 주소", "sender-address-tooltip": "이는 수신자가 이메일을 받을 때 볼 수 있는 이메일 주소입니다. 일반적으로 계정과 연결된 이메일 주소입니다.", @@ -1596,7 +1588,7 @@ "dry-run-step": "모의 실행", "final-import-step": "마지막 단계", "comicvine-parsing-label": "Comic Vine 시리즈 매칭 사용", - "cbl-repo": "커뮤니티에서 많은 독서 목록을 찾을 수 있습니다.
    보고.", + "cbl-repo": "커뮤니티에서 많은 독서 목록을 찾을 수 있습니다. 리포지토리.", "help-label": "{{common.help}}" }, "pdf-reader": { @@ -2015,7 +2007,8 @@ "sidenav": "사이드 내비게이션", "external-sources": "외부 소스", "smart-filters": "스마트 필터", - "help": "{{common.help}}" + "help": "{{common.help}}", + "description": "순서 바꾸기, 표시 여부 전환, 스마트 필터 및 외부 소스를 홈페이지 또는 사이드 탐색 메뉴에 바인딩하여 Kavita의 다양한 기능을 사용자 지정할 수 있습니다." }, "customize-dashboard-streams": { "no-data": "대시보드에 모든 스마트 필터가 추가되었거나 아직 생성되지 않았습니다.", @@ -2258,7 +2251,8 @@ "bulk-delete-libraries": "{{count}}개의 라이브러리를 삭제하시겠습니까?", "webtoon-override": "웹툰을 나타내는 이미지가 있어 웹툰 모드로 전환합니다.", "match-success": "시리즈가 올바르게 일치함", - "confirm-delete-multiple-volumes": "{{count}}개의 볼륨을 삭제하시겠습니까? 디스크의 파일은 수정되지 않습니다." + "confirm-delete-multiple-volumes": "{{count}}개의 볼륨을 삭제하시겠습니까? 디스크의 파일은 수정되지 않습니다.", + "scrobble-gen-init": "과거 열람 이력 및 평가를 추적하는 이벤트를 생성하여 연결된 서비스와 동기화하는 기능을 작업 예정 목록에 추가하였습니다." }, "read-time-pipe": { "less-than-hour": "<1시간", @@ -2330,7 +2324,10 @@ "title": "작업", "copy-settings": "설정 복사하기", "match": "일치", - "match-tooltip": "Kavita+에서 시리즈를 수동으로 일치시키기" + "match-tooltip": "Kavita+에서 시리즈를 수동으로 일치시키기", + "reorder": "정렬 변경", + "rename": "이름 변경", + "rename-tooltip": "스마트 필터 이름 변경" }, "preferences": { "left-to-right": "왼쪽에서 오른쪽", @@ -2587,7 +2584,18 @@ "enable-genres-tooltip": "시리즈 장르를 작성할 수 있도록 허용합니다.", "whitelist-label": "화이트리스트 태그", "age-rating-mapping-title": "연령 등급 매핑", - "genre": "장르" + "genre": "장르", + "enable-chapter-title-label": "제목", + "chapter-header": "챕터 필드", + "enable-chapter-title-tooltip": "Chapter/Issue 제목 작성 허용", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-release-date-label": "발매일", + "enable-chapter-release-date-tooltip": "Chapter/Issue 발매일 작성 허용", + "enable-chapter-publisher-label": "출판사", + "enable-chapter-publisher-tooltip": "Chapter/Issue 출판사 작성 허용", + "enable-chapter-cover-label": "챕터 표지", + "enable-chapter-cover-tooltip": "Chapter/Issue 커버 설정 허용" }, "match-series-modal": { "search": "검색", @@ -2606,7 +2614,8 @@ "details": "페이지 보기", "updating-metadata-status": "메타데이터 업데이트", "releasing": "출시", - "chapter-count": "{{common.chapter-count}}" + "chapter-count": "{{common.chapter-count}}", + "issue-count": "{{common.issue-count}}" }, "email-history": { "sent-header": "성공", @@ -2627,7 +2636,12 @@ "people": "{{tabs.people-tab}}", "summary": "{{filter-field-pipe.summary}}", "publication-status": "{{edit-series-modal.publication-status-title}}", - "start-date": "{{manage-metadata-settings.enable-start-date-label}}" + "start-date": "{{manage-metadata-settings.enable-start-date-label}}", + "chapter-publisher": "{{person-role-pipe.publisher}} (Chapter)", + "chapter-covers": "커버 (Chapter)", + "chapter-title": "제목 (챕터)", + "chapter-release-date": "발매일 (Chapter)", + "chapter-summary": "요약 (Chapter)" }, "role-localized-pipe": { "admin": "관리자", @@ -2645,5 +2659,18 @@ "trace": "추적", "warning": "경고", "critical": "치명적" + }, + "reviews": { + "user-reviews-local": "로컬 리뷰", + "user-reviews-plus": "외부 리뷰" + }, + "review-modal": { + "title": "리뷰 편집", + "review-label": "리뷰", + "close": "{{common.close}}", + "save": "{{common.save}}", + "delete": "{{common.delete}}", + "min-length": "리뷰는 최소 {{count}}자 이상이어야 합니다", + "required": "{{validation.required-field}}" } } diff --git a/UI/Web/src/assets/langs/ms.json b/UI/Web/src/assets/langs/ms.json index 00b1bf9f9..caadc9d33 100644 --- a/UI/Web/src/assets/langs/ms.json +++ b/UI/Web/src/assets/langs/ms.json @@ -27,12 +27,6 @@ "filter-label": "{{common.filter}}", "special": "{{entity-title.special}}" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}" }, diff --git a/UI/Web/src/assets/langs/nb_NO.json b/UI/Web/src/assets/langs/nb_NO.json index 1ecb88f36..ac0ed2c76 100644 --- a/UI/Web/src/assets/langs/nb_NO.json +++ b/UI/Web/src/assets/langs/nb_NO.json @@ -16,12 +16,6 @@ "filter-label": "{{common.filter}}", "special": "{{entity-title.special}}" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}" }, diff --git a/UI/Web/src/assets/langs/nl.json b/UI/Web/src/assets/langs/nl.json index cb0cd1287..57f96555a 100644 --- a/UI/Web/src/assets/langs/nl.json +++ b/UI/Web/src/assets/langs/nl.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, klik om te tonen" }, - "review-series-modal": { - "title": "Beoordeling bewerken", - "review-label": "Beoordeling", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Review moet minimum {{count}} characters hebben", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}s Recensie", diff --git a/UI/Web/src/assets/langs/pl.json b/UI/Web/src/assets/langs/pl.json index 797dcc27b..654b6dcb4 100644 --- a/UI/Web/src/assets/langs/pl.json +++ b/UI/Web/src/assets/langs/pl.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, kliknij by wyświetlić" }, - "review-series-modal": { - "title": "Edytuj recenzję", - "review-label": "Recenzja", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Recenzja musi zawierać co najmniej {{count}} znaków", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Recenzja {{username}}", @@ -762,8 +753,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "W tym tomie nie ma rozdziałów. Nie można odczytać.", "cover-change": "Odświeżenie obrazu w przeglądarce może potrwać do minuty. Do tego czasu na niektórych stronach może być wyświetlany stary obraz.", - "user-reviews-local": "Lokalne recenzje", - "user-reviews-plus": "Zewnętrzne recenzje", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/pt.json b/UI/Web/src/assets/langs/pt.json index 2f4d291cc..5acfea812 100644 --- a/UI/Web/src/assets/langs/pt.json +++ b/UI/Web/src/assets/langs/pt.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, clique para ver" }, - "review-series-modal": { - "title": "Editar Crítica", - "review-label": "Crítica", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "A resenha deve ter pelo menos {{count}} caracteres", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Crítica de {{username}}", @@ -762,8 +753,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "Não existem capítulos neste volume. Impossível ler.", "cover-change": "Pode levar até um minuto para a imagem ser refrescada pelo browser. Até isso acontecer, a imagem antiga será mostrada nalgumas páginas.", - "user-reviews-local": "Críticas Locais", - "user-reviews-plus": "Críticas Externas", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", diff --git a/UI/Web/src/assets/langs/pt_BR.json b/UI/Web/src/assets/langs/pt_BR.json index dd2ad5db1..bf6029d14 100644 --- a/UI/Web/src/assets/langs/pt_BR.json +++ b/UI/Web/src/assets/langs/pt_BR.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spoiler, clique para mostrar" }, - "review-series-modal": { - "title": "Editar Análise", - "review-label": "Análise", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "A análise deve ter pelo menos {{count}} caracteres", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}'s Análise", @@ -81,7 +72,8 @@ "your-review": "Esta é sua análise", "external-review": "Análise Externa", "local-review": "Análise Local", - "rating-percentage": "Avaliação {{r}}%" + "rating-percentage": "Avaliação {{r}}%", + "critic": "crítico" }, "want-to-read": { "title": "Quero Ler", @@ -762,8 +754,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "Não há capítulos neste volume. Não posso ler.", "cover-change": "Pode levar até um minuto para o seu navegador atualizar a imagem. Até lá, a imagem antiga pode ser exibida em algumas páginas.", - "user-reviews-local": "Análises Locais", - "user-reviews-plus": "Análises Externas", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", @@ -825,7 +815,8 @@ "entry-label": "Ver Detalhes", "kavita-tooltip": "Sua Avaliação + Geral", "kavita-rating-title": "Sua Avaliação", - "close": "{{common.close}}" + "close": "{{common.close}}", + "critic": "{{review-card.critic}}" }, "badge-expander": { "more-items": "e {{count}} mais" @@ -1522,7 +1513,8 @@ "logout": "Sair", "all-filters": "Filtros Inteligentes", "nav-link-header": "Opções de Navegação", - "close": "{{common.close}}" + "close": "{{common.close}}", + "person-aka-status": "Resultados como pseudônimos" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -2261,7 +2253,8 @@ "match-success": "Série correspondida corretamente", "webtoon-override": "Mudando para o modo Webtoon devido a imagens que representam um Webtoon.", "scrobble-gen-init": "Enfileirou uma tarefa para gerar eventos scrobble a partir do histórico de leitura e avaliações anteriores, sincronizando-os com serviços conectados.", - "confirm-delete-multiple-volumes": "Tem certeza de que deseja excluir {{count}} volumes? Isso não modificará os arquivos no disco." + "confirm-delete-multiple-volumes": "Tem certeza de que deseja excluir {{count}} volumes? Isso não modificará os arquivos no disco.", + "series-added-want-to-read": "Série adicionada da lista Quero Ler" }, "read-time-pipe": { "less-than-hour": "<1 Hora", @@ -2336,7 +2329,8 @@ "match-tooltip": "Corresponder Séries com Kavita+ manualmente", "reorder": "Reordenar", "rename-tooltip": "Renomear o Filtro Inteligente", - "rename": "Renomear" + "rename": "Renomear", + "merge": "Mesclar" }, "preferences": { "left-to-right": "Esquerda para Direita", @@ -2485,7 +2479,11 @@ "cover-image-description-extra": "Alternativamente, você pode baixar uma capa do CoversDB, se disponível.", "download-coversdb": "Baixar do CoversDB", "hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}", - "asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}" + "asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}", + "alias-overlap": "Esse pseudônimo já aponta para outra pessoa ou é o nome dessa pessoa, considere mesclá -la.", + "aliases-label": "Editar pseudônimos", + "aliases-tab": "Pseudônimos", + "aliases-tooltip": "Quando uma série é etiquetada com um pseudônimo de uma pessoa, a pessoa é atribuída em vez de ser criada uma nova pessoa. Ao eliminar um pseudônimo, terá de voltar a analisar a série para que a alteração seja detectada." }, "browse-authors": { "author-count": "{{num}} Pessoas", @@ -2498,7 +2496,9 @@ "individual-role-title": "Como um {{role}}", "known-for-title": "Conhecido Por", "all-roles": "Papéis", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "no-info": "Nenhuma informação sobre esta Pessoa", + "aka-title": "Também conhecido como · " }, "manage-user-tokens": { "anilist-header": "AniList", @@ -2561,7 +2561,7 @@ "save": "{{common.save}}", "no-results": "Não foi possível encontrar uma correspondência. Tente adicionar o URL de um provedor compatível e tente novamente.", "query-label": "Consulta", - "query-tooltip": "Digite o nome da série, URL AniList/MyAnimeList. As URLs usarão uma pesquisa direta.", + "query-tooltip": "Digite o nome da série, urls de Anilist/Myanimelist/ComicbookRoundup URL. As URLs serão usadas como pesquisa direta.", "dont-match-label": "Não Fazer Correspondência", "dont-match-tooltip": "Opte por esta série de correspondência e scrobbling", "search": "Pesquisar" @@ -2571,7 +2571,8 @@ "chapter-count": "{{common.chapter-count}}", "releasing": "Lançando", "details": "Exibir página", - "updating-metadata-status": "Atualizando Metadados" + "updating-metadata-status": "Atualizando Metadados", + "issue-count": "{{common.issue-count}}" }, "email-history": { "description": "Aqui você encontra todos os e-mails enviados por Kavita e para qual usuário.", @@ -2622,7 +2623,18 @@ "enable-cover-image-tooltip": "Permitir que Kavita escreva a imagem da capa das Séries", "localized-name-tooltip": "Permitir que o nome localizado seja escrito quando o campo for desbloqueado. Kavita tentará dar o melhor palpite.", "overrides-description": "Permita que Kavita escreva os campos trancados.", - "overrides-label": "Substituir" + "overrides-label": "Substituir", + "enable-chapter-title-label": "Título", + "enable-chapter-publisher-label": "Editora", + "enable-chapter-publisher-tooltip": "Permitir que a Editora do Capítulo/Número seja salvo", + "enable-chapter-cover-label": "Capa do Capítulo", + "enable-chapter-cover-tooltip": "Permitir que a Capa do Capítulo/Número seja definida", + "chapter-header": "Campos de Capítulo", + "enable-chapter-title-tooltip": "Permitir que o Título do Capítulo/Número seja salvo", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-release-date-label": "Data de Lançamento", + "enable-chapter-release-date-tooltip": "Permitir que a Data de Lançamento do Capítulo/Número seja salvo" }, "metadata-setting-field-pipe": { "publication-status": "{{edit-series-modal.publication-status-title}}", @@ -2633,7 +2645,12 @@ "localized-name": "{{edit-series-modal.localized-name-label}}", "age-rating": "{{metadata-fields.age-rating-title}}", "people": "{{tabs.people-tab}}", - "summary": "{{filter-field-pipe.summary}}" + "summary": "{{filter-field-pipe.summary}}", + "chapter-summary": "Resumo (Capítulo)", + "chapter-covers": "Capas (Capítulo)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Capítulo)", + "chapter-title": "Título (Capítulo)", + "chapter-release-date": "Data de Lançamento (Capítulo)" }, "role-localized-pipe": { "admin": "Admin", @@ -2651,5 +2668,27 @@ "trace": "Rastro", "warning": "Aviso", "critical": "Crítico" + }, + "reviews": { + "user-reviews-local": "Análises Locais", + "user-reviews-plus": "Análises Externas" + }, + "review-modal": { + "title": "Editar Análise", + "review-label": "Análise", + "close": "{{common.close}}", + "delete": "{{common.delete}}", + "min-length": "A análise deve ter pelo menos {{count}} caracteres", + "required": "{{validation.required-field}}", + "save": "{{common.save}}" + }, + "merge-person-modal": { + "save": "{{common.save}}", + "src": "Mesclar Pessoa", + "alias-title": "Novos pseudônimos", + "close": "{{common.close}}", + "title": "{{personName}}", + "merge-warning": "se prosseguir, a pessoa selecionada será removida. O nome da pessoa selecionada será adicionado como um pseudônimo e todas as suas funções serão transferidas.", + "known-for-title": "Conhecido por" } } diff --git a/UI/Web/src/assets/langs/ru.json b/UI/Web/src/assets/langs/ru.json index c190e99b9..0789c6685 100644 --- a/UI/Web/src/assets/langs/ru.json +++ b/UI/Web/src/assets/langs/ru.json @@ -59,15 +59,6 @@ "spoiler": { "click-to-show": "Спойлер, нажмите, чтобы показать" }, - "review-series-modal": { - "title": "Редактировать отзыв", - "review-label": "Обзор", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "Отзыв должен содержать не меньше {{count}} символов", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Обзор на {{username}}", diff --git a/UI/Web/src/assets/langs/sk.json b/UI/Web/src/assets/langs/sk.json index 4d3c36c17..ac2f5ec6a 100644 --- a/UI/Web/src/assets/langs/sk.json +++ b/UI/Web/src/assets/langs/sk.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "Spojler, kliknite na zobrazenie" }, - "review-series-modal": { - "title": "Upraviť recenziu", - "review-label": "Recenzia", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}", - "min-length": "Recenzia musí obsahovať aspoň {{count}} znakov" - }, "review-card-modal": { "close": "{{common.close}}", "external-mod": "(externé)", @@ -784,8 +775,6 @@ "more-alt": "Viac", "publication-status-title": "Publikácia", "publication-status-tooltip": "Stav publikácie", - "user-reviews-local": "Miestne recenzie", - "user-reviews-plus": "Externé recenzie", "pages-count": "{{num}} Strán", "words-count": "{{num}} Slov", "weblinks-title": "Odkazy", diff --git a/UI/Web/src/assets/langs/sv.json b/UI/Web/src/assets/langs/sv.json index c987293a1..f18657fe4 100644 --- a/UI/Web/src/assets/langs/sv.json +++ b/UI/Web/src/assets/langs/sv.json @@ -3,7 +3,7 @@ "username": "{{common.username}}", "password": "{{common.password}}", "password-validation": "{{validation.password-validation}}", - "title": "Logga in till ditt konto", + "title": "Logga in på ditt konto", "forgot-password": "Glömt lösenord?", "submit": "Logga in" }, @@ -43,15 +43,6 @@ "token-expired": "Din AniList-token är Utgången! Scrobbling kommer inte processeras förrän du har förnyat det under Konto.", "generate-scrobble-events": "Backfill Events" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}", - "title": "Redigera recension", - "review-label": "Recension", - "min-length": "Recension måste vara minst {{count}} tecken" - }, "review-card-modal": { "close": "{{common.close}}", "external-mod": "(extern)", @@ -86,7 +77,7 @@ "locale-label": "Språk", "locale-tooltip": "Språket Kavita ska nyttja", "blur-unread-summaries-label": "Censurera olästa sammanfattningar", - "blur-unread-summaries-tooltip": "Cencurera sammanfattningar på volymer eller kapitel som inte har någon läsprogress (för att undvika spoilers)", + "blur-unread-summaries-tooltip": "Cencurera handlingen på volymer eller kapitel som inte har någon läsprogress (för att undvika spoilers)", "disable-animations-tooltip": "Inaktivera animationer på sidan. Användbart på e-ink-läsare.", "reading-mode-label": "Läsläge", "layout-mode-label": "Layoutläge", @@ -269,7 +260,7 @@ "age-restriction-label": "Åldersbegränsning" }, "scrobbling-providers": { - "instructions": "First time users should click on \"{{scrobbling-providers.generate}}\" below to allow Kavita+ to talk with {{service}}. Once you authorize the program, copy and paste the token in the input below. You can regenerate your token at any time.", + "instructions": "Nya användare bör klicka på \"{{scrobbling-providers.generate}}\" nedan för att tillåta Kavita+ att prata med {{service}}. Efter att du gett programmet åtkomst, kopiera och klistra in token i rutan nedanför. Du kan generera nytt token när som helst.", "edit": "{{common.edit}}", "cancel": "{{common.cancel}}", "save": "{{common.save}}", @@ -504,7 +495,7 @@ "incognito-mode-label": "Inkognitoläge", "next": "Nästa", "next-chapter": "Nästa Kapitel/Volym", - "close-reader": "Stäng läsare", + "close-reader": "Stäng Läsare", "go-to-page": "Gå till sida", "go-to-last-page": "Gå till sista sidan", "prev-chapter": "Förra Kapitel/Volym", @@ -601,8 +592,6 @@ "cover-change": "Det kan ta upp till en minut för att din webbläsare ska uppdatera bilden. Under tiden kan den gamla bilden visas på vissa sidor.", "layout-mode-option-list": "Lista", "read-incognito": "Läs inkognito", - "user-reviews-plus": "Externa recensioner", - "user-reviews-local": "Lokala recensioner", "continue-incognito": "Fortsätt inkognito", "incognito": "Inkognito", "continue-from": "Fortsätt {{title}}", @@ -636,7 +625,7 @@ "editors-title": "Redaktörer", "colorists-title": "Färgläggare", "letterers-title": "Bokstavsättare", - "inkers-title": "Inker" + "inkers-title": "Inkers" }, "external-rating": { "close": "{{common.close}}", @@ -944,7 +933,7 @@ "bookmark-dir-tooltip": "Plats där bokmärken kommer att lagras. Bokmärken är källfiler och kan vara stora. Välj en plats med tillräckligt med lagring. Katalogen hanteras; andra filer inom katalogen kommer att raderas. Om du använder Docker, montera en extra volym och använd den.", "encode-as-description-part-2": "Kan jag använda WebP?", "media-issue-title": "Medieproblem", - "scrobble-issue-title": "Scrobble-problem", + "scrobble-issue-title": "Scrobble Nummer", "cover-image-size-label": "Omslagsbildsstorlek" }, "manage-scrobble-errors": { @@ -1110,7 +1099,7 @@ "filter-label": "{{common.filter}}", "promote-tooltip": "Marknadsför betyder att taggen kan ses over hela servern, inte bara för admin-användare. Alla serier som har denna tagg kommer fortfarande att ha användarbegränsningar placerade på sig.", "title": "Redigera {{collectionName}} Samling", - "summary-label": "Summering", + "summary-label": "Handling", "missing-series-title": "Saknade Serier:", "name-validation": "Namn måste vara unikt", "name-label": "Namn", @@ -1295,7 +1284,7 @@ "close": "{{common.close}}", "save": "{{common.save}}", "required-field": "{{validation.required-field}}", - "summary-label": "Summering", + "summary-label": "Handling", "promote-label": "Tipsa", "promote-tooltip": "Tipsa betyder att samlingen kan ses över hela servern, inte bara av dig. Alla serier inom denna samling kommer fortfarande att ha användarbegränsningar som gäller för dem.", "starting-title": "Startar", @@ -1494,7 +1483,7 @@ "highest-count-tooltip": "Högsta Antal funnet i alla ComicInfo i serien", "name-label": "Namn", "sort-name-label": "Sortera Namn", - "summary-label": "Summering", + "summary-label": "Handling", "total-words-title": "Totalt antal Ord", "size-title": "Storlek", "specials-volume": "Specials", @@ -1667,7 +1656,7 @@ "filter": "{{common.filter}}", "clear": "{{common.clear}}", "external-sources-title": "{{customize-dashboard-modal.external-sources}}", - "reorder-when-filter-present": "You cannot reorder items via drag & drop while a filter is present. Use {{customize-sidenav-streams.order-numbers-label}}", + "reorder-when-filter-present": "Du kan inte ordna om objekt via drag & släpp medan ett filter används. Använd {{customize-sidenav-streams.order-numbers-label}}", "order-numbers-label": "{{reading-list-detail.order-numbers-label}}", "bulk-mode-label": "Massläge", "no-data-external-source": "Alla Externa Källor har lagts till i Sido Nav eller så har inga skapats än.", @@ -1752,7 +1741,7 @@ "imprint": "Förlag", "average-rating": "Genomsnittligt betyg", "series-name": "Serie Namn", - "summary": "Summering", + "summary": "Handling", "read-date": "Läsdatum", "want-to-read": "Vill Läsa", "publication-status": "Publikationsstatus" @@ -1823,13 +1812,15 @@ "remove-from-on-deck-tooltip": "Radera serier från att visas på Hyllan", "add-to-collection-tooltip": "Lägg till serier i en samling", "delete-tooltip": "Det finns inget sätt att ångra detta beslut", - "back-to": "Tillbaka till {{action}}" + "back-to": "Tillbaka till {{action}}", + "rename": "Byt namn", + "rename-tooltip": "Byt namn på Smart Filter" }, "dashboard": { "recently-added-title": "Nyligen tillagda serier", "more-in-genre-title": "Mer inom {{genre}}", "server-settings-link": "Serverinställningar", - "no-libraries": "Det är inga bibliotek skapade än. Skapa några i", + "no-libraries": "Det finns inga bibliotek skapade än. Skapa några i", "not-granted": "Du har inte fått rättigheter till något bibliotek.", "on-deck-title": "På hyllan", "recently-updated-title": "Nyligen uppdaterade serier" @@ -1866,7 +1857,7 @@ }, "manage-metadata-settings": { "enabled-tooltip": "Tillåt Kavita att ladda ner metadata och skriva till sin databas.", - "summary-label": "Summering", + "summary-label": "Handling", "enable-people-label": "Personer", "enable-start-date-tooltip": "Tillåt Seriens startdatum att bli skriven till serien", "enable-genres-label": "Genres", @@ -1887,7 +1878,7 @@ "enable-genres-tooltip": "Tillåt serie-genrer att skrivas.", "description": "Kavita+ har möjlighet att ladda ner och skriva viss begränsad metadata till databasen. Denna sida gör det möjligt för dig att växla vad som ingår.", "enable-people-tooltip": "Tillåt personer (karaktärer, författare, osv.) att läggas till. Alla personer inkluderar bilder.", - "summary-tooltip": "TIllåt Summering att skrivas när fältet är upplåst.", + "summary-tooltip": "Tillåt Handling att skrivas när fältet är upplåst.", "derive-publication-status-tooltip": "Tillåt publiceringsstatus att härledas från totala kapitel-/volymantal.", "derive-publication-status-label": "Publiceringsstatus", "enable-relations-label": "Förhållanden", @@ -1903,7 +1894,18 @@ "enable-tags-label": "Taggar", "age-rating-mapping-title": "Åldersgruppsbedömning", "add-age-rating-mapping-label": "Lägg till Åldersgruppsbedömning", - "field-mapping-title": "Fältmappning" + "field-mapping-title": "Fältmappning", + "enable-chapter-title-label": "Titel", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-title-tooltip": "Tillåt Titeln för Kapitel/Nummer att skrivas", + "enable-chapter-cover-tooltip": "Tillåt Omslag för Kapitel/Nummer att ställas in", + "enable-chapter-cover-label": "Kapitelomslag", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-release-date-label": "Släppdatum", + "enable-chapter-release-date-tooltip": "Tillåt Släppdatum för Kapitel/Nummer att skrivas", + "enable-chapter-publisher-tooltip": "Tillåt Utgivare för Kapitel/Nummer att skrivas", + "enable-chapter-publisher-label": "Utgivare", + "chapter-header": "Kapitelfält" }, "match-series-result-item": { "releasing": "Släpper", @@ -2129,7 +2131,7 @@ "show": "Visa", "hide": "Göm", "regen-warning": "Att regenerera din API-nyckel kommer att ogiltigförklara alla befintliga klienter.", - "no-key": "FEL – NYCKEL INTE ANGETTS", + "no-key": "ERROR – NYCKEL HAR EJ ANGETTS", "confirm-reset": "Detta kommer att ogiltigförklara alla OPDS-konfigurationer du har ställt in. Är du säker på att du vill fortsätta?", "key-reset": "Återställ API-nyckel", "copy": "Kopiera" @@ -2168,7 +2170,7 @@ "sunday": "Söndag" }, "device-platform-pipe": { - "custom": "Anpassa" + "custom": "Anpassad" }, "cbl-import-result-pipe": { "success": "Lyckats", @@ -2648,6 +2650,11 @@ "start-date": "{{manage-metadata-settings.enable-start-date-label}}", "genres": "{{metadata-fields.genres-title}}", "tags": "{{metadata-fields.tags-title}}", - "localized-name": "{{edit-series-modal.localized-name-label}}" + "localized-name": "{{edit-series-modal.localized-name-label}}", + "chapter-summary": "Handling (Kapitel)", + "chapter-publisher": "{{person-role-pipe.publisher}} (Kapitel)", + "chapter-title": "Titel (Kapitel)", + "chapter-release-date": "Släppdatum (Kapitel)", + "chapter-covers": "Omslag (Kapitel)" } } diff --git a/UI/Web/src/assets/langs/ta.json b/UI/Web/src/assets/langs/ta.json index d72580f42..5b61fd060 100644 --- a/UI/Web/src/assets/langs/ta.json +++ b/UI/Web/src/assets/langs/ta.json @@ -75,15 +75,6 @@ "spoiler": { "click-to-show": "ச்பாய்லர், காட்ட சொடுக்கு செய்க" }, - "review-series-modal": { - "title": "மதிப்பாய்வைத் திருத்தவும்", - "review-label": "சீராய்வு", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "மதிப்பாய்வு குறைந்தபட்சம் {{count}} எழுத்துக்களாக இருக்க வேண்டும்", - "required": "{{validation.required-field}}" - }, "theme-manager": { "set-default": "இயல்புநிலையை அமைக்கவும்", "default-theme": "இயல்புநிலை", @@ -237,7 +228,6 @@ "kavita+-requirement": "கவிதா+ அண்மைக் கால வெளியீடு - 2 பதிப்புகளுடன் மட்டுமே வேலை செய்ய வடிவமைக்கப்பட்டுள்ளது. அதற்கு வெளியே எதுவும் வேலை செய்யாததற்கு உட்பட்டது." }, "series-detail": { - "user-reviews-plus": "வெளிப்புற மதிப்புரைகள்", "writers-title": "{{metadata-fields.writers-title}}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}}", "characters-title": "{{metadata-fields.Characters-title}}}", @@ -251,7 +241,6 @@ "tags-title": "{{metadata-fields.tags-title}}}", "ongoing": "{{வெளியீட்டு-நிலை-பைப்.ங்கோயிங்}}", "release-date-title": "வெளியீடு", - "user-reviews-local": "உள்ளக மதிப்புரைகள்", "page-settings-title": "பக்க அமைப்புகள்", "close": "{{common.close}}", "layout-mode-label": "{{பயனர்-முன்னுரிமைகள்.லேவுட்-மோட்-புக்-லேபிள்}}}", diff --git a/UI/Web/src/assets/langs/th.json b/UI/Web/src/assets/langs/th.json index 274b53b69..059c707e6 100644 --- a/UI/Web/src/assets/langs/th.json +++ b/UI/Web/src/assets/langs/th.json @@ -55,14 +55,6 @@ "spoiler": { "click-to-show": "คลิกเพื่อแสดง" }, - "review-series-modal": { - "title": "แก้ไขรีวิว", - "review-label": "รีวิว", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "รีวิวของ {{username}}", diff --git a/UI/Web/src/assets/langs/tr.json b/UI/Web/src/assets/langs/tr.json index 7964d8303..eb08d66df 100644 --- a/UI/Web/src/assets/langs/tr.json +++ b/UI/Web/src/assets/langs/tr.json @@ -49,14 +49,6 @@ "spoiler": { "click-to-show": "Spoiler, görmek için tıklayın" }, - "review-series-modal": { - "title": "İncelemeyi düzenle", - "review-label": "İnceleme", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}} İncelemesi", @@ -122,7 +114,9 @@ "clients-api-key-tooltip": "API anahtarı bir şifre gibidir. Sıfırlamak mevcut istemcileri geçersiz kılar.", "clients-opds-url-tooltip": "Desteklenen OPDS istemcilerinin listesine bakın: ", "reset": "{{common.reset}}", - "save": "{{common.save}}" + "save": "{{common.save}}", + "pdf-reader-settings-title": "PDF okuyucu", + "kavitaplus-settings-title": "Kavita+" }, "user-holds": { "no-data": "{{typeahead.no-data}}" @@ -138,7 +132,9 @@ "delete": "{{common.delete}}", "drag-n-drop": "{{cover-image-chooser.drag-n-drop}}", "upload": "{{cover-image-chooser.upload}}", - "add": "{{common.add}}" + "add": "{{common.add}}", + "downloadable": "İndirilebilir", + "downloaded": "İndirildi" }, "theme": { "theme-dark": "Karanlık", @@ -183,7 +179,10 @@ "edit": "{{common.edit}}", "cancel": "{{common.cancel}}", "save": "{{common.save}}", - "required-field": "{{validation.required-field}}" + "required-field": "{{validation.required-field}}", + "confirm-password-label": "Parola onayı", + "current-password-label": "Geçerli parola", + "new-password-label": "Yeni parola" }, "change-email": { "required-field": "{{validation.required-field}}", @@ -192,7 +191,10 @@ "edit": "{{common.edit}}", "cancel": "{{common.cancel}}", "save": "{{common.save}}", - "setup-user-account": "Kullanıcının hesabını kur" + "setup-user-account": "Kullanıcının hesabını kur", + "email-title": "E-posta", + "email-label": "Yeni e-posta", + "current-password-label": "Geçerli parola" }, "change-age-restriction": { "reset": "{{common.reset}}", @@ -258,12 +260,16 @@ "email-label": "{{common.email}}", "required-field": "{{validation.required-field}}", "valid-email": "{{validation.valid-email}}", - "submit": "{{common.submit}}" + "submit": "{{common.submit}}", + "title": "Parolayı sıfırla" }, "reset-password-modal": { "close": "{{common.close}}", "cancel": "{{common.cancel}}", - "save": "{{common.save}}" + "save": "{{common.save}}", + "title": "{{username}}'ın parolasını sıfırla", + "new-password-label": "Yeni parola", + "error-label": "Hata: " }, "all-series": { "series-count": "{{common.series-count}}" @@ -295,7 +301,8 @@ "activate-email-label": "{{common.email}}", "activate-delete": "{{common.delete}}", "activate-reset": "Lisansı Sıfırla", - "activate-save": "{{common.save}}" + "activate-save": "{{common.save}}", + "reset-label": "Sıfırla" }, "book-line-overlay": { "close": "{{common.close}}", @@ -317,7 +324,9 @@ "password-label": "{{common.password}}", "required-field": "{{validation.required-field}}", "submit": "{{common.submit}}", - "password-validation": "{{validation.password-validation}}" + "password-validation": "{{validation.password-validation}}", + "description": "Yeni parola girin", + "title": "Parolayı sıfırla" }, "register": { "username-label": "{{common.username}}", @@ -362,7 +371,7 @@ "volume-num": "{{common.volume-num}}", "reading-lists-title": "{{side-nav.reading-lists}}", "time-to-read-alt": "{{sort-field-pipe.time-to-read}}", - "scrobbling-tooltip": "{{settings.scrobbling}}" + "scrobbling-tooltip": "{{settings.scrobbling}}: {{value}}" }, "metadata-fields": { "collections-title": "{{side-nav.collections}}", @@ -393,7 +402,8 @@ "cancel": "{{common.cancel}}", "save": "{{common.save}}", "required-field": "{{validation.required-field}}", - "help": "{{common.help}}" + "help": "{{common.help}}", + "force-scan": "Taramaya zorla" }, "reader-settings": { "font-family-label": "{{user-preferences.font-family-label}}", @@ -403,7 +413,8 @@ "reading-direction-label": "{{user-preferences.reading-direction-book-label}}", "writing-style-label": "{{user-preferences.writing-style-label}}", "immersive-mode-label": "{{user-preferences.immersive-mode-label}}", - "layout-mode-label": "{{user-preferences.layout-mode-book-label}}" + "layout-mode-label": "{{user-preferences.layout-mode-book-label}}", + "reset-to-defaults": "Varsayılanlara sıfırla" }, "bookmarks": { "title": "{{side-nav.bookmarks}}", @@ -438,7 +449,8 @@ "cover-image-chooser": { "reset": "{{common.reset}}", "apply": "{{common.apply}}", - "applied": "{{theme-manager.applied}}" + "applied": "{{theme-manager.applied}}", + "reset-cover-tooltip": "Kapak görselini sıfırla" }, "edit-series-relation": { "remove": "{{common.remove}}", @@ -747,7 +759,8 @@ "manage-smart-filters": { "delete": "{{common.delete}}", "filter": "{{common.filter}}", - "clear": "{{common.clear}}" + "clear": "{{common.clear}}", + "cancel": "{{common.cancel}}" }, "edit-external-source-item": { "save": "{{common.save}}", @@ -777,7 +790,8 @@ "writers": "{{metadata-fields.writers-title}}" }, "actionable": { - "clear": "{{common.clear}}" + "clear": "{{common.clear}}", + "scan-library-tooltip": "Değişiklikler için kütüphaneyi tarayın. Her klasörü kontrol etmek için taramayı zorla kullanın" }, "preferences": { "1-column": "1 Sütun", @@ -802,18 +816,30 @@ "username": "Kullanıcı adı", "password": "Parola", "select-all": "Tümünü Seç", - "book-num": "Kitap" + "book-num": "Kitap", + "cancel": "İptal", + "reset-to-default": "Varsayılan olarak sıfırla" }, "tabs": { "account-tab": "Hesap" }, "toasts": { "account-registration-complete": "Hesap kaydı tamamlandı", - "account-migration-complete": "Hesap taşıma tamamlandı" + "account-migration-complete": "Hesap taşıma tamamlandı", + "reset-ip-address": "IP adresleri sıfırla", + "password-reset": "Parola sıfırla", + "email-service-reset": "E-posta hizmeti sıfırla" }, "api-key": { "hide": "Gizle", "show": "Göster", - "copy": "Kopyala" + "copy": "Kopyala", + "reset": "Sıfırla" + }, + "confirm": { + "cancel": "{{common.cancel}}" + }, + "publication-status-pipe": { + "cancelled": "İptal edildi" } } diff --git a/UI/Web/src/assets/langs/uk.json b/UI/Web/src/assets/langs/uk.json index 0dc9a0a87..37e2c1131 100644 --- a/UI/Web/src/assets/langs/uk.json +++ b/UI/Web/src/assets/langs/uk.json @@ -48,15 +48,6 @@ "not-processed": "Не опрацьовано", "chapter-num": "Розділ" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}", - "title": "Редагувати огляд", - "review-label": "Огляд", - "min-length": "Огляд мусить містити принаймні {{count}} знаків" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "Огляд від {{username}}", diff --git a/UI/Web/src/assets/langs/vi.json b/UI/Web/src/assets/langs/vi.json index 7a559a5f1..2dd4fbc96 100644 --- a/UI/Web/src/assets/langs/vi.json +++ b/UI/Web/src/assets/langs/vi.json @@ -39,15 +39,6 @@ "processed": "Đã Xử Lý", "not-processed": "Chưa Xử Lý" }, - "review-series-modal": { - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "required": "{{validation.required-field}}", - "title": "Sửa Đánh Giá", - "review-label": "Đánh Giá", - "min-length": "Đánh giá phải dài ít nhất {{count}} ký tự" - }, "review-card-modal": { "close": "{{common.close}}", "go-to-review": "Tới Phần Đánh giá", @@ -562,8 +553,6 @@ "read-options-alt": "Tùy Chọn Đọc", "edit-series-alt": "Chỉnh Sửa Thông Tin", "send-to": "Tệp được gửi qua email tới {{deviceName}}", - "user-reviews-local": "Đánh Giá Cục Bộ", - "user-reviews-plus": "Đánh Giá Bên Ngoài", "release-date-title": "Phát hành", "pages-count": "{{num}} Trang", "words-count": "{{num}} Từ", diff --git a/UI/Web/src/assets/langs/zh_Hans.json b/UI/Web/src/assets/langs/zh_Hans.json index 191d29891..af222921d 100644 --- a/UI/Web/src/assets/langs/zh_Hans.json +++ b/UI/Web/src/assets/langs/zh_Hans.json @@ -62,15 +62,6 @@ "spoiler": { "click-to-show": "剧透,点击显示" }, - "review-series-modal": { - "title": "编辑评论", - "review-label": "评论", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "评论必须至少有 {{count}} 个字符", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}的评论", @@ -81,7 +72,8 @@ "your-review": "我的评论", "external-review": "外部评论", "local-review": "本地评论", - "rating-percentage": "评分 {{r}}%" + "rating-percentage": "评分 {{r}}%", + "critic": "评论家" }, "want-to-read": { "title": "想读", @@ -106,7 +98,7 @@ "locale-label": "本地语言", "locale-tooltip": "Kavita 当前语言", "blur-unread-summaries-label": "模糊未读摘要", - "blur-unread-summaries-tooltip": "模糊没有阅读进度的卷或章节的摘要文本(避免剧透)", + "blur-unread-summaries-tooltip": "模糊没有阅读进度的卷或章节的内容简介文本(避免剧透)", "prompt-on-download-label": "下载时提示", "prompt-on-download-tooltip": "当下载大小超过 {{size}}MB 时提示", "disable-animations-label": "关闭动画", @@ -762,8 +754,6 @@ "no-pages": "{{toasts.no-pages}}", "no-chapters": "本卷没有章节,无法读取。", "cover-change": "刷新图片需要一点时间。在此期间,可能某些页面仍显示旧图片。", - "user-reviews-local": "本地评论", - "user-reviews-plus": "外部评论", "writers-title": "{{metadata-fields.writers-title}}", "cover-artists-title": "{{metadata-fields.cover-artists-title}}", "characters-title": "{{metadata-fields.characters-title}}", @@ -825,7 +815,8 @@ "entry-label": "查看详情", "kavita-tooltip": "您的评分 + 总体", "kavita-rating-title": "您的评分", - "close": "{{common.close}}" + "close": "{{common.close}}", + "critic": "{{review-card.critic}}" }, "badge-expander": { "more-items": "和{{count}}个更多" @@ -1522,7 +1513,8 @@ "logout": "注销", "all-filters": "智能筛选", "nav-link-header": "导航选项", - "close": "{{common.close}}" + "close": "{{common.close}}", + "person-aka-status": "匹配别名" }, "promoted-icon": { "promoted": "{{common.promoted}}" @@ -2261,7 +2253,8 @@ "match-success": "系列匹配正确", "webtoon-override": "由于图片代表网络漫画,因此切换到网络漫画模式。", "scrobble-gen-init": "将一项任务加入队列,该任务将根据过去的阅读历史和评分生成 scrobble 事件,并将其与已连接的服务同步。", - "confirm-delete-multiple-volumes": "您确定要删除这 {{count}} 个卷吗?这不会修改磁盘上的文件。" + "confirm-delete-multiple-volumes": "您确定要删除这 {{count}} 个卷吗?这不会修改磁盘上的文件。", + "series-added-want-to-read": "从“想读”列表中添加的系列" }, "read-time-pipe": { "less-than-hour": "<1 小时", @@ -2336,7 +2329,8 @@ "match-tooltip": "使用 Kavita+ 手动匹配系列", "reorder": "重新排序", "rename-tooltip": "重命名智能筛选", - "rename": "重命名" + "rename": "重命名", + "merge": "合并" }, "preferences": { "left-to-right": "从左到右", @@ -2485,7 +2479,11 @@ "cover-image-description-extra": "或者,您可以从 CoversDB 下载封面(如果有)。", "download-coversdb": "从 CoversDB 下载", "mal-tooltip": "https://myanimelist.net/people/{MalId}/", - "hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}" + "hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}", + "aliases-tooltip": "当某个系列被标记为某个人的别名时,系统会分配该人,而不是创建新的人。删除别名后,您必须重新扫描该系列才能生效。", + "aliases-tab": "别名", + "aliases-label": "编辑别名", + "alias-overlap": "此别名已经指向另一个人或为该人的名字,请考虑合并它们。" }, "person-detail": { "all-roles": "角色", @@ -2493,7 +2491,9 @@ "individual-role-title": "作为 {{role}}", "browse-person-title": "{{name}} 的全部作品", "browse-person-by-role-title": "{{name}} 作为 {{role}} 的所有作品", - "anilist-url": "{{edit-person-modal.anilist-tooltip}}" + "anilist-url": "{{edit-person-modal.anilist-tooltip}}", + "no-info": "没有关于此人的信息", + "aka-title": "又名 " }, "browse-authors": { "author-count": "{{num}} 人", @@ -2507,7 +2507,7 @@ "description": "选择一个匹配项来重新连接 Kavita+ 元数据并重新生成 scrobble 事件。 不匹配可用于限制 Kavita 匹配元数据和乱序。", "no-results": "无法找到匹配项。尝试添加来自受支持的提供商的 URL,然后重试。", "query-label": "询问", - "query-tooltip": "输入系列名称、AniList/MyAnimeList url。 URL 将使用直接查找。", + "query-tooltip": "输入系列名称、AniList/MyAnimeList/ComicBookRoundup 网址。网址将使用直接查找。", "dont-match-label": "不匹配", "dont-match-tooltip": "从匹配和 scrobbling 中选择该系列", "search": "搜索" @@ -2571,7 +2571,8 @@ "chapter-count": "{{common.chapter-count}}", "releasing": "释放", "details": "查看页面", - "updating-metadata-status": "更新元数据" + "updating-metadata-status": "更新元数据", + "issue-count": "{{common.issue-count}}" }, "email-history": { "description": "在这里您可以找到从 Kavita 发送的所有电子邮件以及发送给哪个用户。", @@ -2622,7 +2623,18 @@ "enable-cover-image-label": "封面图片", "enable-cover-image-tooltip": "允许 Kavita 为该系列撰写封面图片", "overrides-label": "覆盖", - "overrides-description": "允许 Kavita 覆盖锁定的字段。" + "overrides-description": "允许 Kavita 覆盖锁定的字段。", + "enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}", + "enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}", + "enable-chapter-release-date-label": "发布日期", + "enable-chapter-release-date-tooltip": "允许写入章节/期刊的发布日期", + "enable-chapter-cover-label": "章节封面", + "enable-chapter-cover-tooltip": "允许设置章节/期刊的封面", + "chapter-header": "章节字段", + "enable-chapter-publisher-label": "出版社", + "enable-chapter-title-label": "标题", + "enable-chapter-title-tooltip": "允许写入章节/期刊的标题", + "enable-chapter-publisher-tooltip": "允许写入章节/期刊的出版社" }, "metadata-setting-field-pipe": { "covers": "封面", @@ -2633,7 +2645,12 @@ "genres": "{{metadata-fields.genres-title}}", "tags": "{{metadata-fields.tags-title}}", "localized-name": "{{edit-series-modal.localized-name-label}}", - "publication-status": "{{edit-series-modal.publication-status-title}}" + "publication-status": "{{edit-series-modal.publication-status-title}}", + "chapter-summary": "内容简介(章节)", + "chapter-publisher": "{{person-role-pipe.publisher}} (章节)", + "chapter-title": "标题(章节)", + "chapter-covers": "封面(章节)", + "chapter-release-date": "发布日期(章节)" }, "role-localized-pipe": { "download": "下载", @@ -2651,5 +2668,27 @@ "trace": "追踪", "warning": "警告", "critical": "严重" + }, + "reviews": { + "user-reviews-plus": "外部评审", + "user-reviews-local": "本地评论" + }, + "review-modal": { + "review-label": "评论", + "save": "{{common.save}}", + "delete": "{{common.delete}}", + "min-length": "评论必须至少包含 {{count}} 个字符", + "required": "{{validation.required-field}}", + "title": "编辑评论", + "close": "{{common.close}}" + }, + "merge-person-modal": { + "close": "{{common.close}}", + "src": "合并人员", + "title": "{{personName}}", + "known-for-title": "代表作品", + "merge-warning": "如果继续,所选人员将被移除。所选人员的姓名将被添加为别名,并且其所有角色都将被转移。", + "alias-title": "新别名", + "save": "{{common.save}}" } } diff --git a/UI/Web/src/assets/langs/zh_Hant.json b/UI/Web/src/assets/langs/zh_Hant.json index 1e36a2940..8e5841f26 100644 --- a/UI/Web/src/assets/langs/zh_Hant.json +++ b/UI/Web/src/assets/langs/zh_Hant.json @@ -58,15 +58,6 @@ "spoiler": { "click-to-show": "點擊查看據透" }, - "review-series-modal": { - "title": "編輯評論", - "review-label": "評論", - "close": "{{common.close}}", - "save": "{{common.save}}", - "delete": "{{common.delete}}", - "min-length": "評論至少需有 {{count}} 個字元", - "required": "{{validation.required-field}}" - }, "review-card-modal": { "close": "{{common.close}}", "user-review": "{{username}}的評論", @@ -712,8 +703,6 @@ "page-settings-title": "頁面設定", "layout-mode-option-list": "清單", "incognito": "無痕模式", - "user-reviews-local": "本地評論", - "user-reviews-plus": "外部評論", "release-date-title": "發行", "publication-status-title": "出版", "continue-from": "繼續 {{title}}", diff --git a/UI/Web/src/theme/themes/dark.scss b/UI/Web/src/theme/themes/dark.scss index 0abecf3da..5e3c4aead 100644 --- a/UI/Web/src/theme/themes/dark.scss +++ b/UI/Web/src/theme/themes/dark.scss @@ -437,4 +437,11 @@ --login-input-font-family: 'League Spartan', sans-serif; --login-input-placeholder-opacity: 0.5; --login-input-placeholder-color: #fff; + + /** Series Detail **/ + --detail-subtitle-color: lightgrey; + + /** Search **/ + --input-hint-border-color: #aeaeae; + --input-hint-text-color: lightgrey; } diff --git a/build.sh b/build.sh index 673620e3b..b1b739af4 100755 --- a/build.sh +++ b/build.sh @@ -94,9 +94,11 @@ Package() fi echo "Copying appsettings.json" - cp config/appsettings.json $lOutputFolder/config/appsettings.json + cp config/appsettings.json $lOutputFolder/config/appsettings-init.json echo "Removing appsettings.Development.json" rm $lOutputFolder/config/appsettings.Development.json + echo "Removing appsettings.json" + rm $lOutputFolder/config/appsettings.json echo "Creating tar" cd ../$outputFolder/"$runtime"/ diff --git a/openapi.json b/openapi.json index cf26aeec8..7cac13947 100644 --- a/openapi.json +++ b/openapi.json @@ -1,13 +1,13 @@ { "openapi": "3.0.4", "info": { - "title": "Kavita (v0.8.6.5)", - "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.5", + "title": "Kavita", + "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.6.9", "license": { "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "3.1.0" + "version": "0.8.6.9" }, "servers": [ { @@ -5469,6 +5469,55 @@ } } }, + "/api/Person/search": { + "get": { + "tags": [ + "Person" + ], + "summary": "Find a person by name or alias against a query string", + "parameters": [ + { + "name": "queryString", + "in": "query", + "description": "", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonDto" + } + } + } + } + } + } + } + }, "/api/Person/roles": { "get": { "tags": [ @@ -5844,6 +5893,105 @@ } } }, + "/api/Person/merge": { + "post": { + "tags": [ + "Person" + ], + "summary": "Merges Persons into one, this action is irreversible", + "requestBody": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonMergeDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PersonMergeDto" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PersonMergeDto" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PersonDto" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonDto" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PersonDto" + } + } + } + } + } + } + }, + "/api/Person/valid-alias": { + "get": { + "tags": [ + "Person" + ], + "summary": "Ensure the alias is valid to be added. For example, the alias cannot be on another person or be the same as the current person name/alias.", + "parameters": [ + { + "name": "personId", + "in": "query", + "description": "", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "alias", + "in": "query", + "description": "", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "boolean" + } + }, + "application/json": { + "schema": { + "type": "boolean" + } + }, + "text/json": { + "schema": { + "type": "boolean" + } + } + } + } + } + } + }, "/api/Plugin/authenticate": { "post": { "tags": [ @@ -16660,6 +16808,13 @@ "type": "string", "nullable": true }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "description": { "type": "string", "nullable": true @@ -17115,18 +17270,15 @@ }, "range": { "type": "string", - "description": "Range of chapters. Chapter 2-4 -> \"2-4\". Chapter 2 -> \"2\". If special, will be special name.", "nullable": true }, "number": { "type": "string", - "description": "Smallest number of the Range.", "nullable": true, "deprecated": true }, "minNumber": { "type": "number", - "description": "This may be 0 under the circumstance that the Issue is \"Alpha\" or other non-standard numbers.", "format": "float" }, "maxNumber": { @@ -17135,21 +17287,17 @@ }, "sortOrder": { "type": "number", - "description": "The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.", "format": "float" }, "pages": { "type": "integer", - "description": "Total number of pages in all MangaFiles", "format": "int32" }, "isSpecial": { - "type": "boolean", - "description": "If this Chapter contains files that could only be identified as Series or has Special Identifier from filename" + "type": "boolean" }, "title": { "type": "string", - "description": "Used for books/specials to display custom title. For non-specials/books, will be set to API.DTOs.ChapterDto.Range", "nullable": true }, "files": { @@ -17176,17 +17324,14 @@ "format": "date-time" }, "coverImageLocked": { - "type": "boolean", - "description": "If the Cover Image is locked for this entity" + "type": "boolean" }, "volumeId": { "type": "integer", - "description": "Volume Id this Chapter belongs to", "format": "int32" }, "createdUtc": { "type": "string", - "description": "When chapter was created", "format": "date-time" }, "lastModifiedUtc": { @@ -17195,22 +17340,18 @@ }, "created": { "type": "string", - "description": "When chapter was created in local server time", "format": "date-time" }, "releaseDate": { "type": "string", - "description": "When the chapter was released.", "format": "date-time" }, "titleName": { "type": "string", - "description": "Title of the Chapter/Issue", "nullable": true }, "summary": { "type": "string", - "description": "Summary of the Chapter", "nullable": true }, "ageRating": { @@ -17233,12 +17374,11 @@ -1 ], "type": "integer", - "description": "Age Rating for the issue/chapter", + "description": "Represents Age Rating for content.", "format": "int32" }, "wordCount": { "type": "integer", - "description": "Total words in a Chapter (books only)", "format": "int64" }, "volumeTitle": { @@ -17260,12 +17400,10 @@ }, "webLinks": { "type": "string", - "description": "Comma-separated link of urls to external services that have some relation to the Chapter", "nullable": true }, "isbn": { "type": "string", - "description": "ISBN-13 (usually) of the Chapter", "nullable": true }, "writers": { @@ -17387,17 +17525,14 @@ }, "language": { "type": "string", - "description": "Language for the Chapter/Issue", "nullable": true }, "count": { "type": "integer", - "description": "Number in the TotalCount of issues", "format": "int32" }, "totalCount": { "type": "integer", - "description": "Total number of issues for the series", "format": "int32" }, "languageLocked": { @@ -17407,12 +17542,10 @@ "type": "boolean" }, "ageRatingLocked": { - "type": "boolean", - "description": "Locked by user so metadata updates from scan loop will not override AgeRating" + "type": "boolean" }, "publicationStatusLocked": { - "type": "boolean", - "description": "Locked by user so metadata updates from scan loop will not override PublicationStatus" + "type": "boolean" }, "genresLocked": { "type": "boolean" @@ -18643,6 +18776,7 @@ 1 ], "type": "integer", + "description": "Where this rating comes from: Critic or User", "format": "int32" }, "providerUrl": { @@ -20926,6 +21060,13 @@ "type": "string", "nullable": true }, + "aliases": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PersonAlias" + }, + "nullable": true + }, "coverImage": { "type": "string", "nullable": true @@ -20983,6 +21124,35 @@ }, "additionalProperties": false }, + "PersonAlias": { + "required": [ + "alias", + "normalizedAlias" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "alias": { + "type": "string", + "nullable": true + }, + "normalizedAlias": { + "type": "string", + "nullable": true + }, + "personId": { + "type": "integer", + "format": "int32" + }, + "person": { + "$ref": "#/components/schemas/Person" + } + }, + "additionalProperties": false + }, "PersonDto": { "required": [ "name" @@ -21012,6 +21182,13 @@ "type": "string", "nullable": true }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "description": { "type": "string", "nullable": true @@ -21039,6 +21216,26 @@ }, "additionalProperties": false }, + "PersonMergeDto": { + "required": [ + "destId", + "srcId" + ], + "type": "object", + "properties": { + "destId": { + "type": "integer", + "description": "The id of the person being merged into", + "format": "int32" + }, + "srcId": { + "type": "integer", + "description": "The id of the person being merged. This person will be removed, and become an alias of API.DTOs.PersonMergeDto.DestId", + "format": "int32" + } + }, + "additionalProperties": false + }, "PersonalToCDto": { "required": [ "chapterId", @@ -22691,7 +22888,6 @@ }, "lastChapterAdded": { "type": "string", - "description": "DateTime representing last time a chapter was added to the Series", "format": "date-time" }, "userRating": { @@ -22719,9 +22915,6 @@ "type": "string", "format": "date-time" }, - "nameLocked": { - "type": "boolean" - }, "sortNameLocked": { "type": "boolean" }, @@ -22730,7 +22923,6 @@ }, "wordCount": { "type": "integer", - "description": "Total number of words for the series. Only applies to epubs.", "format": "int64" }, "libraryId": { @@ -22755,26 +22947,21 @@ }, "folderPath": { "type": "string", - "description": "The highest level folder for this Series", "nullable": true }, "lowestFolderPath": { "type": "string", - "description": "Lowest path (that is under library root) that contains all files for the series.", "nullable": true }, "lastFolderScanned": { "type": "string", - "description": "The last time the folder for this series was scanned", "format": "date-time" }, "dontMatch": { - "type": "boolean", - "description": "Do not match the series with any external Metadata service. This will automatically opt it out of scrobbling." + "type": "boolean" }, "isBlacklisted": { - "type": "boolean", - "description": "If the series was unable to match, it will be blacklisted until a manual metadata match overrides it" + "type": "boolean" }, "coverImage": { "type": "string", @@ -24023,18 +24210,15 @@ }, "range": { "type": "string", - "description": "Range of chapters. Chapter 2-4 -> \"2-4\". Chapter 2 -> \"2\". If special, will be special name.", "nullable": true }, "number": { "type": "string", - "description": "Smallest number of the Range.", "nullable": true, "deprecated": true }, "minNumber": { "type": "number", - "description": "This may be 0 under the circumstance that the Issue is \"Alpha\" or other non-standard numbers.", "format": "float" }, "maxNumber": { @@ -24043,21 +24227,17 @@ }, "sortOrder": { "type": "number", - "description": "The sorting order of the Chapter. Inherits from MinNumber, but can be overridden.", "format": "float" }, "pages": { "type": "integer", - "description": "Total number of pages in all MangaFiles", "format": "int32" }, "isSpecial": { - "type": "boolean", - "description": "If this Chapter contains files that could only be identified as Series or has Special Identifier from filename" + "type": "boolean" }, "title": { "type": "string", - "description": "Used for books/specials to display custom title. For non-specials/books, will be set to API.DTOs.ChapterDto.Range", "nullable": true }, "files": { @@ -24084,17 +24264,14 @@ "format": "date-time" }, "coverImageLocked": { - "type": "boolean", - "description": "If the Cover Image is locked for this entity" + "type": "boolean" }, "volumeId": { "type": "integer", - "description": "Volume Id this Chapter belongs to", "format": "int32" }, "createdUtc": { "type": "string", - "description": "When chapter was created", "format": "date-time" }, "lastModifiedUtc": { @@ -24103,22 +24280,18 @@ }, "created": { "type": "string", - "description": "When chapter was created in local server time", "format": "date-time" }, "releaseDate": { "type": "string", - "description": "When the chapter was released.", "format": "date-time" }, "titleName": { "type": "string", - "description": "Title of the Chapter/Issue", "nullable": true }, "summary": { "type": "string", - "description": "Summary of the Chapter", "nullable": true }, "ageRating": { @@ -24141,12 +24314,11 @@ -1 ], "type": "integer", - "description": "Age Rating for the issue/chapter", + "description": "Represents Age Rating for content.", "format": "int32" }, "wordCount": { "type": "integer", - "description": "Total words in a Chapter (books only)", "format": "int64" }, "minHoursToRead": { @@ -24163,12 +24335,10 @@ }, "webLinks": { "type": "string", - "description": "Comma-separated link of urls to external services that have some relation to the Chapter", "nullable": true }, "isbn": { "type": "string", - "description": "ISBN-13 (usually) of the Chapter", "nullable": true }, "writers": { @@ -24290,17 +24460,14 @@ }, "language": { "type": "string", - "description": "Language for the Chapter/Issue", "nullable": true }, "count": { "type": "integer", - "description": "Number in the TotalCount of issues", "format": "int32" }, "totalCount": { "type": "integer", - "description": "Total number of issues for the series", "format": "int32" }, "languageLocked": { @@ -24310,12 +24477,10 @@ "type": "boolean" }, "ageRatingLocked": { - "type": "boolean", - "description": "Locked by user so metadata updates from scan loop will not override AgeRating" + "type": "boolean" }, "publicationStatusLocked": { - "type": "boolean", - "description": "Locked by user so metadata updates from scan loop will not override PublicationStatus" + "type": "boolean" }, "genresLocked": { "type": "boolean" @@ -25124,6 +25289,13 @@ "minLength": 1, "type": "string" }, + "aliases": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "description": { "type": "string", "nullable": true @@ -25542,6 +25714,7 @@ "items": { "type": "string" }, + "description": "List of Roles to assign to user. If admin not present, Pleb will be applied.\nIf admin present, all libraries will be granted access and will ignore those from DTO.", "nullable": true }, "libraries": { @@ -25558,7 +25731,6 @@ }, "email": { "type": "string", - "description": "Email of the user", "nullable": true } }, @@ -25709,7 +25881,6 @@ "locale", "noTransitions", "pageSplitOption", - "pdfLayoutMode", "pdfScrollMode", "pdfSpreadMode", "pdfTheme", @@ -25730,7 +25901,6 @@ 1 ], "type": "integer", - "description": "Manga Reader Option: What direction should the next/prev page buttons go", "format": "int32" }, "scalingOption": { @@ -25741,7 +25911,6 @@ 3 ], "type": "integer", - "description": "Manga Reader Option: How should the image be scaled to screen", "format": "int32" }, "pageSplitOption": { @@ -25752,7 +25921,6 @@ 3 ], "type": "integer", - "description": "Manga Reader Option: Which side of a split image should we show first", "format": "int32" }, "readerMode": { @@ -25762,7 +25930,6 @@ 2 ], "type": "integer", - "description": "Manga Reader Option: How the manga reader should perform paging or reading of the file\n\nWebtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging\nby clicking top/bottom sides of reader.\n", "format": "int32" }, "layoutMode": { @@ -25772,57 +25939,45 @@ 3 ], "type": "integer", - "description": "Manga Reader Option: How many pages to display in the reader at once", "format": "int32" }, "emulateBook": { - "type": "boolean", - "description": "Manga Reader Option: Emulate a book by applying a shadow effect on the pages" + "type": "boolean" }, "backgroundColor": { "minLength": 1, - "type": "string", - "description": "Manga Reader Option: Background color of the reader" + "type": "string" }, "swipeToPaginate": { - "type": "boolean", - "description": "Manga Reader Option: Should swiping trigger pagination" + "type": "boolean" }, "autoCloseMenu": { - "type": "boolean", - "description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction" + "type": "boolean" }, "showScreenHints": { - "type": "boolean", - "description": "Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change" + "type": "boolean" }, "allowAutomaticWebtoonReaderDetection": { - "type": "boolean", - "description": "Manga Reader Option: Allow Automatic Webtoon detection" + "type": "boolean" }, "bookReaderMargin": { "type": "integer", - "description": "Book Reader Option: Override extra Margin", "format": "int32" }, "bookReaderLineSpacing": { "type": "integer", - "description": "Book Reader Option: Override line-height", "format": "int32" }, "bookReaderFontSize": { "type": "integer", - "description": "Book Reader Option: Override font size", "format": "int32" }, "bookReaderFontFamily": { "minLength": 1, - "type": "string", - "description": "Book Reader Option: Maps to the default Kavita font-family (inherit) or an override" + "type": "string" }, "bookReaderTapToPaginate": { - "type": "boolean", - "description": "Book Reader Option: Allows tapping on side of screens to paginate" + "type": "boolean" }, "bookReaderReadingDirection": { "enum": [ @@ -25830,7 +25985,6 @@ 1 ], "type": "integer", - "description": "Book Reader Option: What direction should the next/prev page buttons go", "format": "int32" }, "bookReaderWritingStyle": { @@ -25839,7 +25993,7 @@ 1 ], "type": "integer", - "description": "Book Reader Option: What writing style should be used, horizontal or vertical.", + "description": "Represents the writing styles for the book-reader", "format": "int32" }, "theme": { @@ -25859,8 +26013,7 @@ "format": "int32" }, "bookReaderImmersiveMode": { - "type": "boolean", - "description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this." + "type": "boolean" }, "globalPageLayoutMode": { "enum": [ @@ -25868,33 +26021,26 @@ 1 ], "type": "integer", - "description": "Global Site Option: If the UI should layout items as Cards or List items", "format": "int32" }, "blurUnreadSummaries": { - "type": "boolean", - "description": "UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already" + "type": "boolean" }, "promptForDownloadSize": { - "type": "boolean", - "description": "UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB." + "type": "boolean" }, "noTransitions": { - "type": "boolean", - "description": "UI Site Global Setting: Should Kavita disable CSS transitions" + "type": "boolean" }, "collapseSeriesRelationships": { - "type": "boolean", - "description": "When showing series, only parent series or series with no relationships will be returned" + "type": "boolean" }, "shareReviews": { - "type": "boolean", - "description": "UI Site Global Setting: Should series reviews be shared with all users in the server" + "type": "boolean" }, "locale": { "minLength": 1, - "type": "string", - "description": "UI Site Global Setting: The language locale that should be used for the user" + "type": "string" }, "pdfTheme": { "enum": [ @@ -25902,7 +26048,6 @@ 1 ], "type": "integer", - "description": "PDF Reader: Theme of the Reader", "format": "int32" }, "pdfScrollMode": { @@ -25912,16 +26057,7 @@ 3 ], "type": "integer", - "description": "PDF Reader: Scroll mode of the reader", - "format": "int32" - }, - "pdfLayoutMode": { - "enum": [ - 0, - 2 - ], - "type": "integer", - "description": "PDF Reader: Layout Mode of the reader", + "description": "Enum values match PdfViewer's enums", "format": "int32" }, "pdfSpreadMode": { @@ -25931,16 +26067,13 @@ 2 ], "type": "integer", - "description": "PDF Reader: Spread Mode of the reader", "format": "int32" }, "aniListScrobblingEnabled": { - "type": "boolean", - "description": "Kavita+: Should this account have Scrobbling enabled for AniList" + "type": "boolean" }, "wantToReadSync": { - "type": "boolean", - "description": "Kavita+: Should this account have Want to Read Sync enabled" + "type": "boolean" } }, "additionalProperties": false