diff --git a/API.Tests/Services/DirectoryServiceTests.cs b/API.Tests/Services/DirectoryServiceTests.cs index 22c9bd269..0de244cac 100644 --- a/API.Tests/Services/DirectoryServiceTests.cs +++ b/API.Tests/Services/DirectoryServiceTests.cs @@ -879,7 +879,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/"); + var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); Assert.Empty(allFiles); @@ -903,7 +903,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/"); + var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); Assert.Single(allFiles); // Ignore files are not counted in files, only valid extensions @@ -932,7 +932,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/"); + var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); Assert.Equal(2, allFiles.Count); // Ignore files are not counted in files, only valid extensions @@ -956,7 +956,7 @@ public class DirectoryServiceTests var ds = new DirectoryService(Substitute.For>(), fileSystem); - var allFiles = ds.ScanFiles("C:/Data/"); + var allFiles = ds.ScanFiles("C:/Data/", API.Services.Tasks.Scanner.Parser.Parser.SupportedExtensions); Assert.Equal(5, allFiles.Count); diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs index 32ad8f645..a0f5aa90b 100644 --- a/API.Tests/Services/ParseScannedFilesTests.cs +++ b/API.Tests/Services/ParseScannedFilesTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Metadata; +using API.Data.Repositories; using API.Entities; using API.Entities.Enums; using API.Extensions; @@ -251,9 +252,11 @@ public class ParseScannedFilesTests return Task.CompletedTask; } - - await psf.ScanLibrariesForSeries(LibraryType.Manga, - new List() {"C:/Data/"}, "libraryName", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), TrackFiles); + var library = + await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + LibraryIncludes.Folders | LibraryIncludes.FileTypes); + library.Type = LibraryType.Manga; + await psf.ScanLibrariesForSeries(library, new List() {"C:/Data/"}, false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), TrackFiles); Assert.Equal(3, parsedSeries.Values.Count); @@ -289,12 +292,15 @@ public class ParseScannedFilesTests new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); var directoriesSeen = new HashSet(); + var library = + await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + LibraryIncludes.Folders | LibraryIncludes.FileTypes); await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), (files, directoryPath) => { directoriesSeen.Add(directoryPath); return Task.CompletedTask; - }); + }, library); Assert.Equal(2, directoriesSeen.Count); } @@ -308,11 +314,13 @@ public class ParseScannedFilesTests new MockReadingItemService(new DefaultParser(ds)), Substitute.For()); var directoriesSeen = new HashSet(); - await psf.ProcessFiles("C:/Data/", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),(files, directoryPath) => + await psf.ProcessFiles("C:/Data/", false, await _unitOfWork.SeriesRepository.GetFolderPathMap(1), + (files, directoryPath) => { directoriesSeen.Add(directoryPath); return Task.CompletedTask; - }); + }, await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + LibraryIncludes.Folders | LibraryIncludes.FileTypes)); Assert.Single(directoriesSeen); directoriesSeen.TryGetValue("C:/Data/", out var actual); @@ -342,7 +350,8 @@ public class ParseScannedFilesTests callCount++; return Task.CompletedTask; - }); + }, await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + LibraryIncludes.Folders | LibraryIncludes.FileTypes)); Assert.Equal(2, callCount); } @@ -373,7 +382,8 @@ public class ParseScannedFilesTests { callCount++; return Task.CompletedTask; - }); + }, await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1, + LibraryIncludes.Folders | LibraryIncludes.FileTypes)); Assert.Equal(1, callCount); } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index e31e85fa8..aafb6efad 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -84,6 +84,15 @@ public class LibraryController : BaseApiController .WIthAllowScrobbling(dto.AllowScrobbling) .Build(); + library.LibraryFileTypes = dto.FileGroupTypes + .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) + .Distinct() + .ToList(); + library.LibraryExcludePatterns = dto.ExcludePatterns + .Select(t => new LibraryExcludePattern() {Pattern = t, LibraryId = library.Id}) + .Distinct() + .ToList(); + // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type == LibraryType.Comic) { @@ -415,7 +424,7 @@ public class LibraryController : BaseApiController public async Task UpdateLibrary(UpdateLibraryDto dto) { var userId = User.GetUserId(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(dto.Id, LibraryIncludes.Folders | LibraryIncludes.FileTypes); if (library == null) return BadRequest(await _localizationService.Translate(userId, "library-doesnt-exist")); var newName = dto.Name.Trim(); @@ -437,6 +446,15 @@ public class LibraryController : BaseApiController library.ManageCollections = dto.ManageCollections; library.ManageReadingLists = dto.ManageReadingLists; library.AllowScrobbling = dto.AllowScrobbling; + library.LibraryFileTypes = dto.FileGroupTypes + .Select(t => new LibraryFileTypeGroup() {FileTypeGroup = t, LibraryId = library.Id}) + .Distinct() + .ToList(); + + library.LibraryExcludePatterns = dto.ExcludePatterns + .Select(t => new LibraryExcludePattern() {Pattern = t, LibraryId = library.Id}) + .Distinct() + .ToList(); // Override Scrobbling for Comic libraries since there are no providers to scrobble to if (library.Type == LibraryType.Comic) diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs index 7c1d453b8..3da232c47 100644 --- a/API/DTOs/LibraryDto.cs +++ b/API/DTOs/LibraryDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using API.Entities.Enums; @@ -51,4 +52,12 @@ public class LibraryDto /// When showing series, only parent series or series with no relationships will be returned /// public bool CollapseSeriesRelationships { get; set; } = false; + /// + /// The types of file type groups the library will scan for + /// + public ICollection LibraryFileTypes { get; set; } + /// + /// A set of globs that will exclude matching content from being scanned + /// + public ICollection ExcludePatterns { get; set; } } diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs index b7eabf52b..b8e8e4953 100644 --- a/API/DTOs/UpdateLibraryDto.cs +++ b/API/DTOs/UpdateLibraryDto.cs @@ -28,4 +28,13 @@ public class UpdateLibraryDto public bool ManageReadingLists { get; init; } [Required] public bool AllowScrobbling { get; init; } + /// + /// What types of files to allow the scanner to pickup + /// + [Required] + public ICollection FileGroupTypes { get; init; } + /// + /// A set of Glob patterns that the scanner will exclude processing + /// + public ICollection ExcludePatterns { get; init; } } diff --git a/API/Data/ManualMigrations/MigrateBrokenGMT1Dates.cs b/API/Data/ManualMigrations/MigrateBrokenGMT1Dates.cs deleted file mode 100644 index 8b4576696..000000000 --- a/API/Data/ManualMigrations/MigrateBrokenGMT1Dates.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.7 introduced UTC dates and GMT+1 users would sometimes have dates stored as '0000-12-31 23:00:00'. -/// This Migration will update those dates. -/// -// ReSharper disable once InconsistentNaming -public static class MigrateBrokenGMT1Dates -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - // if current version is > 0.7, then we can exit and not perform - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (Version.Parse(settings.InstallVersion) > new Version(0, 7, 0, 2)) - { - return; - } - logger.LogCritical("Running MigrateBrokenGMT1Dates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); - - #region Series - logger.LogInformation("Updating Dates on Series..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Series SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE Series SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - UPDATE Series SET LastChapterAddedUtc = '0001-01-01 00:00:00' WHERE LastChapterAddedUtc = '0000-12-31 23:00:00'; - UPDATE Series SET LastFolderScannedUtc = '0001-01-01 00:00:00' WHERE LastFolderScannedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Series...Done"); - #endregion - - #region Library - logger.LogInformation("Updating Dates on Libraries..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Library SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE Library SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Libraries...Done"); - #endregion - - #region Volume - try - { - logger.LogInformation("Updating Dates on Volumes..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Volume SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE Volume SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Volumes...Done"); - } - catch (Exception ex) - { - logger.LogCritical(ex, "Updating Dates on Volumes...Failed"); - } - #endregion - - #region Chapter - try - { - logger.LogInformation("Updating Dates on Chapters..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Chapter SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE Chapter SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Chapters...Done"); - } - catch (Exception ex) - { - logger.LogCritical(ex, "Updating Dates on Chapters...Failed"); - } - #endregion - - #region AppUserBookmark - logger.LogInformation("Updating Dates on Bookmarks..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE AppUserBookmark SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE AppUserBookmark SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Bookmarks...Done"); - #endregion - - #region AppUserProgress - logger.LogInformation("Updating Dates on Progress..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE AppUserProgresses SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE AppUserProgresses SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Progress...Done"); - #endregion - - #region Device - logger.LogInformation("Updating Dates on Device..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Device SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE Device SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - UPDATE Device SET LastUsedUtc = '0001-01-01 00:00:00' WHERE LastUsedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on Device...Done"); - #endregion - - #region MangaFile - logger.LogInformation("Updating Dates on MangaFile..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE MangaFile SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE MangaFile SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - UPDATE MangaFile SET LastFileAnalysisUtc = '0001-01-01 00:00:00' WHERE LastFileAnalysisUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on MangaFile...Done"); - #endregion - - #region ReadingList - logger.LogInformation("Updating Dates on ReadingList..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE ReadingList SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE ReadingList SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on ReadingList...Done"); - #endregion - - #region SiteTheme - logger.LogInformation("Updating Dates on SiteTheme..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE SiteTheme SET CreatedUtc = '0001-01-01 00:00:00' WHERE CreatedUtc = '0000-12-31 23:00:00'; - UPDATE SiteTheme SET LastModifiedUtc = '0001-01-01 00:00:00' WHERE LastModifiedUtc = '0000-12-31 23:00:00'; - "); - logger.LogInformation("Updating Dates on SiteTheme...Done"); - #endregion - - logger.LogInformation("MigrateBrokenGMT1Dates migration finished"); - - } -} diff --git a/API/Data/ManualMigrations/MigrateChangePasswordRoles.cs b/API/Data/ManualMigrations/MigrateChangePasswordRoles.cs deleted file mode 100644 index 74344775f..000000000 --- a/API/Data/ManualMigrations/MigrateChangePasswordRoles.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using Microsoft.AspNetCore.Identity; - -namespace API.Data.ManualMigrations; - -/// -/// New role introduced in v0.5.1. Adds the role to all users. -/// -public static class MigrateChangePasswordRoles -{ - /// - /// Will not run if any users have the ChangePassword role already - /// - /// - /// - public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager) - { - var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangePasswordRole); - if (usersWithRole.Count != 0) return; - - var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); - foreach (var user in allUsers) - { - await userManager.RemoveFromRoleAsync(user, "ChangePassword"); - await userManager.AddToRoleAsync(user, PolicyConstants.ChangePasswordRole); - } - } -} diff --git a/API/Data/ManualMigrations/MigrateChangeRestrictionRoles.cs b/API/Data/ManualMigrations/MigrateChangeRestrictionRoles.cs deleted file mode 100644 index 0b22b3f23..000000000 --- a/API/Data/ManualMigrations/MigrateChangeRestrictionRoles.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// New role introduced in v0.6. Adds the role to all users. -/// -public static class MigrateChangeRestrictionRoles -{ - /// - /// Will not run if any users have the role already - /// - /// - /// - /// - public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager, ILogger logger) - { - var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.ChangeRestrictionRole); - if (usersWithRole.Count != 0) return; - - logger.LogCritical("Running MigrateChangeRestrictionRoles migration"); - - var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); - foreach (var user in allUsers) - { - await userManager.RemoveFromRoleAsync(user, PolicyConstants.ChangeRestrictionRole); - await userManager.AddToRoleAsync(user, PolicyConstants.ChangeRestrictionRole); - } - - logger.LogInformation("MigrateChangeRestrictionRoles migration complete"); - } -} diff --git a/API/Data/ManualMigrations/MigrateDashboardStreamNamesToLocaleKeys.cs b/API/Data/ManualMigrations/MigrateDashboardStreamNamesToLocaleKeys.cs deleted file mode 100644 index e5e0f365b..000000000 --- a/API/Data/ManualMigrations/MigrateDashboardStreamNamesToLocaleKeys.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.7.8.6 explicitly introduced DashboardStream and v0.7.8.9 changed the default seed titles to use locale strings. -/// This migration will target nightly releases and should be removed before v0.7.9 release. -/// -public static class MigrateDashboardStreamNamesToLocaleKeys -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - var allStreams = await unitOfWork.UserRepository.GetAllDashboardStreams(); - if (!allStreams.Any(s => s.Name.Equals("On Deck"))) return; - - logger.LogCritical("Running MigrateDashboardStreamNamesToLocaleKeys migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); - foreach (var stream in allStreams.Where(s => s.IsProvided)) - { - stream.Name = stream.Name switch - { - "On Deck" => "on-deck", - "Recently Updated" => "recently-updated", - "Newly Added" => "newly-added", - "More In" => "more-in-genre", - _ => stream.Name - }; - unitOfWork.UserRepository.Update(stream); - } - - await unitOfWork.CommitAsync(); - logger.LogInformation("MigrateDashboardStreamNamesToLocaleKeys migration finished"); - } -} diff --git a/API/Data/ManualMigrations/MigrateDisableScrobblingOnComicLIbraries.cs b/API/Data/ManualMigrations/MigrateDisableScrobblingOnComicLIbraries.cs deleted file mode 100644 index a432a1362..000000000 --- a/API/Data/ManualMigrations/MigrateDisableScrobblingOnComicLIbraries.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using API.Entities.Enums; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.7.4 introduced Scrobbling with Kavita+. By default, it is on, but Comic libraries have no scrobble providers, so disable -/// -public static class MigrateDisableScrobblingOnComicLibraries -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - if (!await dataContext.Library.Where(s => s.Type == LibraryType.Comic).Where(l => l.AllowScrobbling).AnyAsync()) - { - return; - } - logger.LogInformation("Running MigrateDisableScrobblingOnComicLibraries migration. Please be patient, this may take some time"); - - - foreach (var lib in await dataContext.Library.Where(s => s.Type == LibraryType.Comic).Where(l => l.AllowScrobbling).ToListAsync()) - { - lib.AllowScrobbling = false; - unitOfWork.LibraryRepository.Update(lib); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - - logger.LogInformation("MigrateDisableScrobblingOnComicLibraries migration finished"); - - } - -} diff --git a/API/Data/ManualMigrations/MigrateExistingRatings.cs b/API/Data/ManualMigrations/MigrateExistingRatings.cs deleted file mode 100644 index 4314c724b..000000000 --- a/API/Data/ManualMigrations/MigrateExistingRatings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.7.5.6 and v0.7.6, Ratings > 0 need to have "HasRatingSet" -/// -/// Added in v0.7.5.6 -// ReSharper disable once InconsistentNaming -public static class MigrateExistingRatings -{ - public static async Task Migrate(DataContext context, ILogger logger) - { - logger.LogCritical("Running MigrateExistingRatings migration - Please be patient, this may take some time. This is not an error"); - - foreach (var r in context.AppUserRating.Where(r => r.Rating > 0f)) - { - r.HasBeenRated = true; - context.Entry(r).State = EntityState.Modified; - } - - if (context.ChangeTracker.HasChanges()) - { - await context.SaveChangesAsync(); - } - - logger.LogCritical("Running MigrateExistingRatings migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs b/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs new file mode 100644 index 000000000..1da2f6303 --- /dev/null +++ b/API/Data/ManualMigrations/MigrateLibrariesToHaveAllFileTypes.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using API.Entities; +using API.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace API.Data.ManualMigrations; + +/// +/// Introduced in v0.7.11 with the removal of .Kavitaignore files +/// +public static class MigrateLibrariesToHaveAllFileTypes +{ + public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) + { + logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Please be patient, this may take some time. This is not an error"); + var allLibs = await dataContext.Library.Include(l => l.LibraryFileTypes).ToListAsync(); + foreach (var library in allLibs.Where(library => library.LibraryFileTypes.Count == 0)) + { + switch (library.Type) + { + case LibraryType.Manga: + case LibraryType.Comic: + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Archive + }); + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Epub + }); + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Images + }); + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Pdf + }); + break; + case LibraryType.Book: + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Pdf + }); + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Epub + }); + break; + case LibraryType.Image: + library.LibraryFileTypes.Add(new LibraryFileTypeGroup() + { + FileTypeGroup = FileTypeGroup.Images + }); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + await dataContext.SaveChangesAsync(); + logger.LogCritical("Running MigrateLibrariesToHaveAllFileTypes migration - Completed. This is not an error"); + } +} diff --git a/API/Data/ManualMigrations/MigrateLoginRole.cs b/API/Data/ManualMigrations/MigrateLoginRole.cs deleted file mode 100644 index 0a582b761..000000000 --- a/API/Data/ManualMigrations/MigrateLoginRole.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Threading.Tasks; -using API.Constants; -using API.Entities; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Added in v0.7.1.18 -/// -public static class MigrateLoginRoles -{ - /// - /// Will not run if any users have the role already - /// - /// - /// - /// - public static async Task Migrate(IUnitOfWork unitOfWork, UserManager userManager, ILogger logger) - { - var usersWithRole = await userManager.GetUsersInRoleAsync(PolicyConstants.LoginRole); - if (usersWithRole.Count != 0) return; - - logger.LogCritical("Running MigrateLoginRoles migration"); - - var allUsers = await unitOfWork.UserRepository.GetAllUsersAsync(); - foreach (var user in allUsers) - { - await userManager.RemoveFromRoleAsync(user, PolicyConstants.LoginRole); - await userManager.AddToRoleAsync(user, PolicyConstants.LoginRole); - } - - logger.LogInformation("MigrateLoginRoles migration complete"); - } -} diff --git a/API/Data/ManualMigrations/MigrateNormalizedEverything.cs b/API/Data/ManualMigrations/MigrateNormalizedEverything.cs deleted file mode 100644 index d5ba39ab6..000000000 --- a/API/Data/ManualMigrations/MigrateNormalizedEverything.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated -/// -public static class MigrateNormalizedEverything -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - // if current version is > 0.5.6.5, then we can exit and not perform - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 5)) - { - return; - } - logger.LogCritical("Running MigrateNormalizedEverything migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); - - logger.LogInformation("Updating Normalization on Series..."); - foreach (var series in await dataContext.Series.ToListAsync()) - { - series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName ?? string.Empty); - series.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.Name ?? string.Empty); - logger.LogInformation("Updated Series: {SeriesName}", series.Name); - unitOfWork.SeriesRepository.Update(series); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - logger.LogInformation("Updating Normalization on Series...Done"); - - // Genres - logger.LogInformation("Updating Normalization on Genres..."); - foreach (var genre in await dataContext.Genre.ToListAsync()) - { - genre.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(genre.Title ?? string.Empty); - logger.LogInformation("Updated Genre: {Genre}", genre.Title); - unitOfWork.GenreRepository.Attach(genre); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - logger.LogInformation("Updating Normalization on Genres...Done"); - - // Tags - logger.LogInformation("Updating Normalization on Tags..."); - foreach (var tag in await dataContext.Tag.ToListAsync()) - { - tag.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(tag.Title ?? string.Empty); - logger.LogInformation("Updated Tag: {Tag}", tag.Title); - unitOfWork.TagRepository.Attach(tag); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - logger.LogInformation("Updating Normalization on Tags...Done"); - - // People - logger.LogInformation("Updating Normalization on People..."); - foreach (var person in await dataContext.Person.ToListAsync()) - { - person.NormalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(person.Name ?? string.Empty); - logger.LogInformation("Updated Person: {Person}", person.Name); - unitOfWork.PersonRepository.Attach(person); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - logger.LogInformation("Updating Normalization on People...Done"); - - // Collections - logger.LogInformation("Updating Normalization on Collections..."); - foreach (var collection in await dataContext.CollectionTag.ToListAsync()) - { - collection.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(collection.Title ?? string.Empty); - logger.LogInformation("Updated Collection: {Collection}", collection.Title); - unitOfWork.CollectionTagRepository.Update(collection); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - logger.LogInformation("Updating Normalization on Collections...Done"); - - // Reading Lists - logger.LogInformation("Updating Normalization on Reading Lists..."); - foreach (var readingList in await dataContext.ReadingList.ToListAsync()) - { - readingList.NormalizedTitle = Services.Tasks.Scanner.Parser.Parser.Normalize(readingList.Title ?? string.Empty); - logger.LogInformation("Updated Reading List: {ReadingList}", readingList.Title); - unitOfWork.ReadingListRepository.Update(readingList); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - logger.LogInformation("Updating Normalization on Reading Lists...Done"); - - - logger.LogInformation("MigrateNormalizedEverything migration finished"); - - } - -} diff --git a/API/Data/ManualMigrations/MigrateNormalizedLocalizedName.cs b/API/Data/ManualMigrations/MigrateNormalizedLocalizedName.cs deleted file mode 100644 index dcb5e9370..000000000 --- a/API/Data/ManualMigrations/MigrateNormalizedLocalizedName.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// v0.5.6 introduced Normalized Localized Name, which allows for faster lookups and less memory usage. This migration will calculate them once -/// -public static class MigrateNormalizedLocalizedName -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - if (!await dataContext.Series.Where(s => s.NormalizedLocalizedName == null).AnyAsync()) - { - return; - } - logger.LogInformation("Running MigrateNormalizedLocalizedName migration. Please be patient, this may take some time"); - - - foreach (var series in await dataContext.Series.ToListAsync()) - { - series.NormalizedLocalizedName = Services.Tasks.Scanner.Parser.Parser.Normalize(series.LocalizedName ?? string.Empty); - logger.LogInformation("Updated {SeriesName} normalized localized name: {LocalizedName}", series.Name, series.NormalizedLocalizedName); - unitOfWork.SeriesRepository.Update(series); - } - - if (unitOfWork.HasChanges()) - { - await unitOfWork.CommitAsync(); - } - - logger.LogInformation("MigrateNormalizedLocalizedName migration finished"); - - } - -} diff --git a/API/Data/ManualMigrations/MigrateReadingListAgeRating.cs b/API/Data/ManualMigrations/MigrateReadingListAgeRating.cs deleted file mode 100644 index 4541801c7..000000000 --- a/API/Data/ManualMigrations/MigrateReadingListAgeRating.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading.Tasks; -using API.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// New role introduced in v0.6. Calculates the Age Rating on all Reading Lists -/// -public static class MigrateReadingListAgeRating -{ - /// - /// Will not run if any above v0.5.6.24 or v0.6.0 - /// - /// - /// - /// - /// - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext context, IReadingListService readingListService, ILogger logger) - { - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 26)) - { - return; - } - - logger.LogInformation("MigrateReadingListAgeRating migration starting"); - var readingLists = await context.ReadingList.Include(r => r.Items).ToListAsync(); - foreach (var readingList in readingLists) - { - await readingListService.CalculateReadingListAgeRating(readingList); - context.ReadingList.Update(readingList); - } - - await context.SaveChangesAsync(); - logger.LogInformation("MigrateReadingListAgeRating migration complete"); - } -} diff --git a/API/Data/ManualMigrations/MigrateRemoveExtraThemes.cs b/API/Data/ManualMigrations/MigrateRemoveExtraThemes.cs deleted file mode 100644 index f893866bd..000000000 --- a/API/Data/ManualMigrations/MigrateRemoveExtraThemes.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using API.Services.Tasks; - -namespace API.Data.ManualMigrations; - -/// -/// In v0.5.3, we removed Light and E-Ink themes. This migration will remove the themes from the DB and default anyone on -/// null, E-Ink, or Light to Dark. -/// -public static class MigrateRemoveExtraThemes -{ - public static async Task Migrate(IUnitOfWork unitOfWork, IThemeService themeService) - { - var themes = (await unitOfWork.SiteThemeRepository.GetThemes()).ToList(); - - if (themes.Find(t => t.Name.Equals("Light")) == null) - { - return; - } - - Console.WriteLine("Removing Dark and E-Ink themes"); - - var darkTheme = themes.Single(t => t.Name.Equals("Dark")); - var lightTheme = themes.Single(t => t.Name.Equals("Light")); - var eInkTheme = themes.Single(t => t.Name.Equals("E-Ink")); - - - - // Update default theme if it's not Dark or a custom theme - await themeService.UpdateDefault(darkTheme.Id); - - // Update all users to Dark theme if they are on Light/E-Ink - foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(lightTheme.Id)) - { - pref.Theme = darkTheme; - } - foreach (var pref in await unitOfWork.UserRepository.GetAllPreferencesByThemeAsync(eInkTheme.Id)) - { - pref.Theme = darkTheme; - } - - // Remove Light/E-Ink themes - foreach (var siteTheme in themes.Where(t => t.Name.Equals("Light") || t.Name.Equals("E-Ink"))) - { - unitOfWork.SiteThemeRepository.Remove(siteTheme); - } - // Commit and call it a day - await unitOfWork.CommitAsync(); - - Console.WriteLine("Completed removing Dark and E-Ink themes"); - } - -} diff --git a/API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs b/API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs deleted file mode 100644 index bbabf1905..000000000 --- a/API/Data/ManualMigrations/MigrateRemoveWebPSettingRows.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; -using API.Entities.Enums; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Added in v0.7.2.7/v0.7.3 in which the ConvertXToWebP Setting keys were removed. This migration will remove them. -/// -public static class MigrateRemoveWebPSettingRows -{ - public static async Task Migrate(IUnitOfWork unitOfWork, ILogger logger) - { - logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - Please be patient, this may take some time. This is not an error"); - - var key = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertBookmarkToWebP); - var key2 = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.ConvertCoverToWebP); - if (key == null && key2 == null) - { - logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - complete. Nothing to do"); - return; - } - - unitOfWork.SettingsRepository.Remove(key); - unitOfWork.SettingsRepository.Remove(key2); - - await unitOfWork.CommitAsync(); - - logger.LogCritical("Running MigrateRemoveWebPSettingRows migration - Completed. This is not an error"); - } -} diff --git a/API/Data/ManualMigrations/MigrateToUtcDates.cs b/API/Data/ManualMigrations/MigrateToUtcDates.cs deleted file mode 100644 index a1e758bdb..000000000 --- a/API/Data/ManualMigrations/MigrateToUtcDates.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.6.1.38 or v0.7.0, -/// -public static class MigrateToUtcDates -{ - public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger logger) - { - // if current version is > 0.6.1.38, then we can exit and not perform - var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (Version.Parse(settings.InstallVersion) > new Version(0, 6, 1, 38)) - { - return; - } - logger.LogCritical("Running MigrateToUtcDates migration. Please be patient, this may take some time depending on the size of your library. Do not abort, this can break your Database"); - - #region Series - logger.LogInformation("Updating Dates on Series..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Series SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc'), - [LastChapterAddedUtc] = datetime([LastChapterAdded], 'utc'), - [LastFolderScannedUtc] = datetime([LastFolderScanned], 'utc') - ; - "); - logger.LogInformation("Updating Dates on Series...Done"); - #endregion - - #region Library - logger.LogInformation("Updating Dates on Libraries..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Library SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc') - ; - "); - logger.LogInformation("Updating Dates on Libraries...Done"); - #endregion - - #region Volume - try - { - logger.LogInformation("Updating Dates on Volumes..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Volume SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc'); - "); - logger.LogInformation("Updating Dates on Volumes...Done"); - } - catch (Exception ex) - { - logger.LogCritical(ex, "Updating Dates on Volumes...Failed"); - } - #endregion - - #region Chapter - try - { - logger.LogInformation("Updating Dates on Chapters..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Chapter SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc') - ; - "); - logger.LogInformation("Updating Dates on Chapters...Done"); - } - catch (Exception ex) - { - logger.LogCritical(ex, "Updating Dates on Chapters...Failed"); - } - #endregion - - #region AppUserBookmark - logger.LogInformation("Updating Dates on Bookmarks..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE AppUserBookmark SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc') - ; - "); - logger.LogInformation("Updating Dates on Bookmarks...Done"); - #endregion - - #region AppUserProgress - logger.LogInformation("Updating Dates on Progress..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE AppUserProgresses SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc') - ; - "); - logger.LogInformation("Updating Dates on Progress...Done"); - #endregion - - #region Device - logger.LogInformation("Updating Dates on Device..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE Device SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc'), - [LastUsedUtc] = datetime([LastUsed], 'utc') - ; - "); - logger.LogInformation("Updating Dates on Device...Done"); - #endregion - - #region MangaFile - logger.LogInformation("Updating Dates on MangaFile..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE MangaFile SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc'), - [LastFileAnalysisUtc] = datetime([LastFileAnalysis], 'utc') - ; - "); - logger.LogInformation("Updating Dates on MangaFile...Done"); - #endregion - - #region ReadingList - logger.LogInformation("Updating Dates on ReadingList..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE ReadingList SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc') - ; - "); - logger.LogInformation("Updating Dates on ReadingList...Done"); - #endregion - - #region SiteTheme - logger.LogInformation("Updating Dates on SiteTheme..."); - await dataContext.Database.ExecuteSqlRawAsync(@" - UPDATE SiteTheme SET - [LastModifiedUtc] = datetime([LastModified], 'utc'), - [CreatedUtc] = datetime([Created], 'utc') - ; - "); - logger.LogInformation("Updating Dates on SiteTheme...Done"); - #endregion - - logger.LogInformation("MigrateToUtcDates migration finished"); - - } -} diff --git a/API/Data/ManualMigrations/MigrateUserProgressLibraryId.cs b/API/Data/ManualMigrations/MigrateUserProgressLibraryId.cs deleted file mode 100644 index 575be95ae..000000000 --- a/API/Data/ManualMigrations/MigrateUserProgressLibraryId.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace API.Data.ManualMigrations; - -/// -/// Introduced in v0.6.1.8 and v0.7, this adds library ids to all User Progress to allow for easier queries against progress -/// -public static class MigrateUserProgressLibraryId -{ - public static async Task Migrate(IUnitOfWork unitOfWork, ILogger logger) - { - logger.LogCritical("Running MigrateUserProgressLibraryId migration - Please be patient, this may take some time. This is not an error"); - - var progress = await unitOfWork.AppUserProgressRepository.GetAnyProgress(); - if (progress == null || progress.LibraryId != 0) - { - logger.LogCritical("Running MigrateUserProgressLibraryId migration - complete. Nothing to do"); - return; - } - - var seriesIdsWithLibraryIds = await unitOfWork.SeriesRepository.GetLibraryIdsForSeriesAsync(); - foreach (var prog in await unitOfWork.AppUserProgressRepository.GetAllProgress()) - { - prog.LibraryId = seriesIdsWithLibraryIds[prog.SeriesId]; - unitOfWork.AppUserProgressRepository.Update(prog); - } - - - await unitOfWork.CommitAsync(); - - logger.LogCritical("Running MigrateSeriesRelationsImport migration - Completed. This is not an error"); - } -} diff --git a/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs b/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs new file mode 100644 index 000000000..ec955717c --- /dev/null +++ b/API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs @@ -0,0 +1,2504 @@ +// +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("20231113215006_LibraryFileTypes")] + partial class LibraryFileTypes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + 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("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .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("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.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("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("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.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.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .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.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.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + 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("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + 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("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.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("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .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("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.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + 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("Promoted") + .HasColumnType("INTEGER"); + + 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("Format") + .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("INTEGER"); + + 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("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .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("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("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + 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("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .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("Provider") + .HasColumnType("INTEGER"); + + 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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + 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("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + 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("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + 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.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.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.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .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.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.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.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + 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("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("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .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("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .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("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("Files"); + + b.Navigation("UserProgress"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("LibraryFileTypes"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + 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/20231113215006_LibraryFileTypes.cs b/API/Data/Migrations/20231113215006_LibraryFileTypes.cs new file mode 100644 index 000000000..7fed106e7 --- /dev/null +++ b/API/Data/Migrations/20231113215006_LibraryFileTypes.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class LibraryFileTypes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LibraryFileTypeGroup", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + LibraryId = table.Column(type: "INTEGER", nullable: false), + FileTypeGroup = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LibraryFileTypeGroup", x => x.Id); + table.ForeignKey( + name: "FK_LibraryFileTypeGroup_Library_LibraryId", + column: x => x.LibraryId, + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_LibraryFileTypeGroup_LibraryId", + table: "LibraryFileTypeGroup", + column: "LibraryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LibraryFileTypeGroup"); + } + } +} diff --git a/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs b/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs new file mode 100644 index 000000000..b53aa8138 --- /dev/null +++ b/API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs @@ -0,0 +1,2536 @@ +// +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("20231117234829_LibraryExcludePatterns")] + partial class LibraryExcludePatterns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + 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("LastActive") + .HasColumnType("TEXT"); + + b.Property("LastActiveUtc") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .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("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.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("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("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.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.Chapter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AgeRating") + .HasColumnType("INTEGER"); + + b.Property("AlternateCount") + .HasColumnType("INTEGER"); + + b.Property("AlternateNumber") + .HasColumnType("TEXT"); + + b.Property("AlternateSeries") + .HasColumnType("TEXT"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("ISBN") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("IsSpecial") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("Range") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeriesGroup") + .HasColumnType("TEXT"); + + b.Property("StoryArc") + .HasColumnType("TEXT"); + + b.Property("StoryArcNumber") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("TitleName") + .HasColumnType("TEXT"); + + b.Property("TotalCount") + .HasColumnType("INTEGER"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.Property("WebLinks") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue(""); + + b.Property("WordCount") + .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.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.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + 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("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("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.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("InkerLocked") + .HasColumnType("INTEGER"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("LanguageLocked") + .HasColumnType("INTEGER"); + + b.Property("LettererLocked") + .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("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.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasColumnType("TEXT"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Person"); + }); + + 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("Promoted") + .HasColumnType("INTEGER"); + + 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("Format") + .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("INTEGER"); + + 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("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("AvgHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("CoverImageLocked") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasColumnType("TEXT"); + + b.Property("Format") + .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("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("SortName") + .HasColumnType("TEXT"); + + b.Property("SortNameLocked") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + 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("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("FileName") + .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("Provider") + .HasColumnType("INTEGER"); + + 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("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("CreatedUtc") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("MaxHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("MinHoursToRead") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("Pages") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.Property("WordCount") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + 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("ChapterPerson", b => + { + b.Property("ChapterMetadatasId") + .HasColumnType("INTEGER"); + + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.HasKey("ChapterMetadatasId", "PeopleId"); + + b.HasIndex("PeopleId"); + + b.ToTable("ChapterPerson"); + }); + + 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("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("PersonSeriesMetadata", b => + { + b.Property("PeopleId") + .HasColumnType("INTEGER"); + + b.Property("SeriesMetadatasId") + .HasColumnType("INTEGER"); + + b.HasKey("PeopleId", "SeriesMetadatasId"); + + b.HasIndex("SeriesMetadatasId"); + + b.ToTable("PersonSeriesMetadata"); + }); + + 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.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.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.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.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.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.AppUser", null) + .WithMany("WantToRead") + .HasForeignKey("AppUserId"); + + 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("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("ChapterPerson", b => + { + b.HasOne("API.Entities.Chapter", null) + .WithMany() + .HasForeignKey("ChapterMetadatasId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .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("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("PersonSeriesMetadata", b => + { + b.HasOne("API.Entities.Person", null) + .WithMany() + .HasForeignKey("PeopleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Metadata.SeriesMetadata", null) + .WithMany() + .HasForeignKey("SeriesMetadatasId") + .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("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("Files"); + + 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.ReadingList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + 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/20231117234829_LibraryExcludePatterns.cs b/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs new file mode 100644 index 000000000..d1dd084f7 --- /dev/null +++ b/API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Data.Migrations +{ + /// + public partial class LibraryExcludePatterns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LibraryExcludePattern", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Pattern = table.Column(type: "TEXT", nullable: true), + LibraryId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LibraryExcludePattern", x => x.Id); + table.ForeignKey( + name: "FK_LibraryExcludePattern_Library_LibraryId", + column: x => x.LibraryId, + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_LibraryExcludePattern_LibraryId", + table: "LibraryExcludePattern", + column: "LibraryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LibraryExcludePattern"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 65e12c0a8..4299492ac 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -15,7 +15,7 @@ namespace API.Data.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.11"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); modelBuilder.Entity("API.Entities.AppRole", b => { @@ -893,6 +893,44 @@ namespace API.Data.Migrations 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") @@ -2057,6 +2095,28 @@ namespace API.Data.Migrations 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") @@ -2436,6 +2496,10 @@ namespace API.Data.Migrations { b.Navigation("Folders"); + b.Navigation("LibraryExcludePatterns"); + + b.Navigation("LibraryFileTypes"); + b.Navigation("Series"); }); diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index 2122de616..6cae8e2d3 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -26,7 +26,8 @@ public enum LibraryIncludes Series = 2, AppUser = 4, Folders = 8, - // Ratings = 16 + FileTypes = 16, + ExcludePatterns = 32 } public interface ILibraryRepository @@ -86,7 +87,9 @@ public class LibraryRepository : ILibraryRepository { return _context.Library .Include(l => l.AppUsers) - .Where(library => library.AppUsers.Any(x => x.UserName.Equals(userName))) + .Include(l => l.LibraryFileTypes) + .Include(l => l.LibraryExcludePatterns) + .Where(library => library.AppUsers.Any(x => x.UserName!.Equals(userName))) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -100,12 +103,10 @@ public class LibraryRepository : ILibraryRepository /// public async Task> GetLibrariesAsync(LibraryIncludes includes = LibraryIncludes.None) { - var query = _context.Library + return await _context.Library .Include(l => l.AppUsers) - .Select(l => l); - - query = AddIncludesToQuery(query, includes); - return await query.ToListAsync(); + .Includes(includes) + .ToListAsync(); } /// @@ -142,11 +143,10 @@ public class LibraryRepository : ILibraryRepository public async Task> GetLibraryForIdsAsync(IEnumerable libraryIds, LibraryIncludes includes = LibraryIncludes.None) { - var query = _context.Library - .Where(x => libraryIds.Contains(x.Id)); - - AddIncludesToQuery(query, includes); - return await query.ToListAsync(); + return await _context.Library + .Where(x => libraryIds.Contains(x.Id)) + .Includes(includes) + .ToListAsync(); } public async Task GetTotalFiles() @@ -190,6 +190,7 @@ public class LibraryRepository : ILibraryRepository { return await _context.Library .Include(f => f.Folders) + .Include(l => l.LibraryFileTypes) .OrderBy(l => l.Name) .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -201,31 +202,12 @@ public class LibraryRepository : ILibraryRepository { var query = _context.Library - .Where(x => x.Id == libraryId); + .Where(x => x.Id == libraryId) + .Includes(includes); - query = AddIncludesToQuery(query, includes); return await query.SingleOrDefaultAsync(); } - private static IQueryable AddIncludesToQuery(IQueryable query, LibraryIncludes includeFlags) - { - if (includeFlags.HasFlag(LibraryIncludes.Folders)) - { - query = query.Include(l => l.Folders); - } - - if (includeFlags.HasFlag(LibraryIncludes.Series)) - { - query = query.Include(l => l.Series); - } - - if (includeFlags.HasFlag(LibraryIncludes.AppUser)) - { - query = query.Include(l => l.AppUsers); - } - - return query.AsSplitQuery(); - } public async Task LibraryExists(string libraryName) { diff --git a/API/Entities/Enums/FileTypeGroup.cs b/API/Entities/Enums/FileTypeGroup.cs new file mode 100644 index 000000000..3d33aa37c --- /dev/null +++ b/API/Entities/Enums/FileTypeGroup.cs @@ -0,0 +1,19 @@ +using System.ComponentModel; + +namespace API.Entities.Enums; + +/// +/// Represents a set of file types that can be scanned +/// +public enum FileTypeGroup +{ + [Description("Archive")] + Archive = 1, + [Description("EPub")] + Epub = 2, + [Description("Pdf")] + Pdf = 3, + [Description("Images")] + Images = 4 + +} diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index f0674b8a9..aa53b6651 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using API.Entities.Enums; using API.Entities.Interfaces; -using Microsoft.EntityFrameworkCore; namespace API.Entities; @@ -44,6 +43,8 @@ public class Library : IEntityDate + + public DateTime Created { get; set; } public DateTime LastModified { get; set; } public DateTime CreatedUtc { get; set; } @@ -57,6 +58,8 @@ public class Library : IEntityDate public ICollection Folders { get; set; } = null!; public ICollection AppUsers { get; set; } = null!; public ICollection Series { get; set; } = null!; + public ICollection LibraryFileTypes { get; set; } = new List(); + public ICollection LibraryExcludePatterns { get; set; } = new List(); public void UpdateLastModified() { diff --git a/API/Entities/LibraryExcludedGlob.cs b/API/Entities/LibraryExcludedGlob.cs new file mode 100644 index 000000000..69bc86342 --- /dev/null +++ b/API/Entities/LibraryExcludedGlob.cs @@ -0,0 +1,10 @@ +namespace API.Entities; + +public class LibraryExcludePattern +{ + public int Id { get; set; } + public string Pattern { get; set; } + + public int LibraryId { get; set; } + public Library Library { get; set; } = null!; +} diff --git a/API/Entities/LibraryFileTypeGroup.cs b/API/Entities/LibraryFileTypeGroup.cs new file mode 100644 index 000000000..a3af30d80 --- /dev/null +++ b/API/Entities/LibraryFileTypeGroup.cs @@ -0,0 +1,12 @@ +using API.Entities.Enums; + +namespace API.Entities; + +public class LibraryFileTypeGroup +{ + public int Id { get; set; } + public FileTypeGroup FileTypeGroup { get; set; } + + public int LibraryId { get; set; } + public Library Library { get; set; } = null!; +} diff --git a/API/Extensions/FileTypeGroupExtensions.cs b/API/Extensions/FileTypeGroupExtensions.cs new file mode 100644 index 000000000..6e533fc4f --- /dev/null +++ b/API/Extensions/FileTypeGroupExtensions.cs @@ -0,0 +1,25 @@ +using System; +using API.Entities.Enums; +using API.Services.Tasks.Scanner.Parser; + +namespace API.Extensions; + +public static class FileTypeGroupExtensions +{ + public static string GetRegex(this FileTypeGroup fileTypeGroup) + { + switch (fileTypeGroup) + { + case FileTypeGroup.Archive: + return Parser.ArchiveFileExtensions; + case FileTypeGroup.Epub: + return Parser.EpubFileExtension; + case FileTypeGroup.Pdf: + return Parser.PdfFileExtension; + case FileTypeGroup.Images: + return Parser.ImageFileExtensions;; + default: + throw new ArgumentOutOfRangeException(nameof(fileTypeGroup), fileTypeGroup, null); + } + } +} diff --git a/API/Extensions/QueryExtensions/IncludesExtensions.cs b/API/Extensions/QueryExtensions/IncludesExtensions.cs index a7f89f96d..a3afb765d 100644 --- a/API/Extensions/QueryExtensions/IncludesExtensions.cs +++ b/API/Extensions/QueryExtensions/IncludesExtensions.cs @@ -173,4 +173,34 @@ public static class IncludesExtensions return queryable.AsSplitQuery(); } + + public static IQueryable Includes(this IQueryable query, LibraryIncludes includeFlags) + { + if (includeFlags.HasFlag(LibraryIncludes.Folders)) + { + query = query.Include(l => l.Folders); + } + + if (includeFlags.HasFlag(LibraryIncludes.FileTypes)) + { + query = query.Include(l => l.LibraryFileTypes); + } + + if (includeFlags.HasFlag(LibraryIncludes.Series)) + { + query = query.Include(l => l.Series); + } + + if (includeFlags.HasFlag(LibraryIncludes.AppUser)) + { + query = query.Include(l => l.AppUsers); + } + + if (includeFlags.HasFlag(LibraryIncludes.ExcludePatterns)) + { + query = query.Include(l => l.LibraryExcludePatterns); + } + + return query.AsSplitQuery(); + } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 9585b92a9..77ab29396 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -208,7 +208,13 @@ public class AutoMapperProfiles : Profile CreateMap() .ForMember(dest => dest.Folders, opt => - opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())); + opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())) + .ForMember(dest => dest.LibraryFileTypes, + opt => + opt.MapFrom(src => src.LibraryFileTypes.Select(l => l.FileTypeGroup))) + .ForMember(dest => dest.ExcludePatterns, + opt => + opt.MapFrom(src => src.LibraryExcludePatterns.Select(l => l.Pattern))); CreateMap() .ForMember(dest => dest.AgeRestriction, diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 8a6bd34aa..ae83ac789 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -64,7 +64,7 @@ public interface IDirectoryService IEnumerable GetDirectories(string folderPath); IEnumerable GetDirectories(string folderPath, GlobMatcher? matcher); string GetParentDirectoryName(string fileOrFolder); - IList ScanFiles(string folderPath, GlobMatcher? matcher = null); + IList ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null); DateTime GetLastWriteTime(string folderPath); GlobMatcher? CreateMatcherFromFile(string filePath); } @@ -646,7 +646,7 @@ public class DirectoryService : IDirectoryService /// /// /// - public IList ScanFiles(string folderPath, GlobMatcher? matcher = null) + public IList ScanFiles(string folderPath, string supportedExtensions, GlobMatcher? matcher = null) { _logger.LogDebug("[ScanFiles] called on {Path}", folderPath); var files = new List(); @@ -667,19 +667,19 @@ public class DirectoryService : IDirectoryService foreach (var directory in directories) { - files.AddRange(ScanFiles(directory, matcher)); + files.AddRange(ScanFiles(directory, supportedExtensions, matcher)); } // Get the matcher from either ignore or global (default setup) if (matcher == null) { - files.AddRange(GetFilesWithCertainExtensions(folderPath, Tasks.Scanner.Parser.Parser.SupportedExtensions)); + files.AddRange(GetFilesWithCertainExtensions(folderPath, supportedExtensions)); } else { var foundFiles = GetFilesWithCertainExtensions(folderPath, - Tasks.Scanner.Parser.Parser.SupportedExtensions) + supportedExtensions) .Where(file => !matcher.ExcludeMatches(FileSystem.FileInfo.New(file).Name)); files.AddRange(foundFiles); } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index 0604cb890..2b93a6cad 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using API.Entities; using API.Entities.Enums; using API.Extensions; using API.Services.Tasks.Scanner.Parser; @@ -77,14 +78,30 @@ public class ParseScannedFiles /// A callback async Task to be called once all files for each folder path are found /// If we should bypass any folder last write time checks on the scan and force I/O public async Task ProcessFiles(string folderPath, bool scanDirectoryByDirectory, - IDictionary> seriesPaths, Func, string,Task> folderAction, bool forceCheck = false) + IDictionary> seriesPaths, Func, string,Task> folderAction, Library library, bool forceCheck = false) { string normalizedPath; + var fileExtensions = string.Join("|", library.LibraryFileTypes.Select(l => l.FileTypeGroup.GetRegex())); if (scanDirectoryByDirectory) { // This is used in library scan, so we should check first for a ignore file and use that here as well var potentialIgnoreFile = _directoryService.FileSystem.Path.Join(folderPath, DirectoryService.KavitaIgnoreFile); var matcher = _directoryService.CreateMatcherFromFile(potentialIgnoreFile); + if (matcher != null) + { + _logger.LogWarning(".kavitaignore found! Ignore files is deprecated in favor of Library Settings. Please update and remove file at {Path}", potentialIgnoreFile); + } + + if (library.LibraryExcludePatterns.Count != 0) + { + matcher ??= new GlobMatcher(); + foreach (var pattern in library.LibraryExcludePatterns) + { + matcher.AddExclude(pattern.Pattern); + } + } + + var directories = _directoryService.GetDirectories(folderPath, matcher).ToList(); foreach (var directory in directories) @@ -97,7 +114,7 @@ public class ParseScannedFiles else { // For a scan, this is doing everything in the directory loop before the folder Action is called...which leads to no progress indication - await folderAction(_directoryService.ScanFiles(directory, matcher), directory); + await folderAction(_directoryService.ScanFiles(directory, fileExtensions, matcher), directory); } } @@ -113,7 +130,7 @@ public class ParseScannedFiles // We need to calculate all folders till library root and see if any kavitaignores var seriesMatcher = BuildIgnoreFromLibraryRoot(folderPath, seriesPaths); - await folderAction(_directoryService.ScanFiles(folderPath, seriesMatcher), folderPath); + await folderAction(_directoryService.ScanFiles(folderPath, fileExtensions, seriesMatcher), folderPath); } /// @@ -268,25 +285,24 @@ public class ParseScannedFiles /// /// This will process series by folder groups. This is used solely by ScanSeries /// - /// + /// This should have the FileTypes included /// - /// /// If true, does a directory scan first (resulting in folders being tackled in parallel), else does an immediate scan files /// A map of Series names -> existing folder paths to handle skipping folders /// Action which returns if the folder was skipped and the infos from said folder /// Defaults to false /// - public async Task ScanLibrariesForSeries(LibraryType libraryType, - IEnumerable folders, string libraryName, bool isLibraryScan, + public async Task ScanLibrariesForSeries(Library library, + IEnumerable folders, bool isLibraryScan, IDictionary> seriesPaths, Func>, Task>? processSeriesInfos, bool forceCheck = false) { - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", libraryName, ProgressEventType.Started)); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Starting", library.Name, ProgressEventType.Started)); foreach (var folderPath in folders) { try { - await ProcessFiles(folderPath, isLibraryScan, seriesPaths, ProcessFolder, forceCheck); + await ProcessFiles(folderPath, isLibraryScan, seriesPaths, ProcessFolder, library, forceCheck); } catch (ArgumentException ex) { @@ -294,7 +310,7 @@ public class ParseScannedFiles } } - await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended)); + await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", library.Name, ProgressEventType.Ended)); return; async Task ProcessFolder(IList files, string folder) @@ -311,13 +327,13 @@ public class ParseScannedFiles await processSeriesInfos.Invoke(new Tuple>(true, parsedInfos)); _logger.LogDebug("[ScannerService] Skipped File Scan for {Folder} as it hasn't changed since last scan", folder); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent("Skipped " + normalizedFolder, libraryName, ProgressEventType.Updated)); + MessageFactory.FileScanProgressEvent("Skipped " + normalizedFolder, library.Name, ProgressEventType.Updated)); return; } _logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, - MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", libraryName, ProgressEventType.Updated)); + MessageFactory.FileScanProgressEvent($"{files.Count} files in {folder}", library.Name, ProgressEventType.Updated)); if (files.Count == 0) { _logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder); @@ -326,7 +342,7 @@ public class ParseScannedFiles var scannedSeries = new ConcurrentDictionary>(); var infos = files - .Select(file => _readingItemService.ParseFile(file, folder, libraryType)) + .Select(file => _readingItemService.ParseFile(file, folder, library.Type)) .Where(info => info != null) .ToList(); diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 8a27076aa..8753cd2ca 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -17,7 +17,9 @@ public static class Parser public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; - private const string BookFileExtensions = @"\.epub|\.pdf"; + public const string EpubFileExtension = @"\.epub"; + public const string PdfFileExtension = @"\.pdf"; + private const string BookFileExtensions = EpubFileExtension + "|" + PdfFileExtension; private const string XmlRegexExtensions = @"\.xml"; public const string MacOsMetadataFileStartsWith = @"._"; diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 08d1f86f4..c69ead9c8 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -198,7 +198,7 @@ public class ScannerService : IScannerService var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId}); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); if (library == null) return; var libraryPaths = library.Folders.Select(f => f.Path).ToList(); if (await ShouldScanSeries(seriesId, library, libraryPaths, series, true) != ScanCancelReason.NoCancel) @@ -229,7 +229,6 @@ public class ScannerService : IScannerService await _eventHub.SendMessageAsync(MessageFactory.Error, MessageFactory.ErrorEvent($"{series.Name} scan aborted", "Files for series are not in a nested folder under library path. Correct this and rescan.")); return; } - } if (string.IsNullOrEmpty(folderPath)) @@ -472,7 +471,7 @@ public class ScannerService : IScannerService public async Task ScanLibrary(int libraryId, bool forceUpdate = false) { var sw = Stopwatch.StartNew(); - var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders); + var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns); var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList(); if (!await CheckMounts(library.Name, libraryFolderPaths)) return; @@ -493,7 +492,7 @@ public class ScannerService : IScannerService await _processSeries.Prime(); - var processTasks = new List>(); + //var processTasks = new List>(); var scanElapsedTime = await ScanFiles(library, libraryFolderPaths, shouldUseLibraryScan, TrackFiles, forceUpdate); @@ -579,7 +578,7 @@ public class ScannerService : IScannerService var foundParsedSeries = new ParsedSeries() { Name = parsedFiles[0].Series, - NormalizedName = Scanner.Parser.Parser.Normalize(parsedFiles[0].Series), + NormalizedName = Parser.Normalize(parsedFiles[0].Series), Format = parsedFiles[0].Format, }; @@ -588,7 +587,7 @@ public class ScannerService : IScannerService seenSeries.AddRange(parsedFiles.Select(pf => new ParsedSeries() { Name = pf.Series, - NormalizedName = Scanner.Parser.Parser.Normalize(pf.Series), + NormalizedName = Parser.Normalize(pf.Series), Format = pf.Format })); return; @@ -616,7 +615,7 @@ public class ScannerService : IScannerService var scanner = new ParseScannedFiles(_logger, _directoryService, _readingItemService, _eventHub); var scanWatch = Stopwatch.StartNew(); - await scanner.ScanLibrariesForSeries(library.Type, dirs, library.Name, + await scanner.ScanLibrariesForSeries(library, dirs, isLibraryScan, await _unitOfWork.SeriesRepository.GetFolderPathMap(library.Id), processSeriesInfos, forceChecks); var scanElapsedTime = scanWatch.ElapsedMilliseconds; diff --git a/API/Startup.cs b/API/Startup.cs index 95e2dba09..c44f6a5d6 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -243,6 +243,7 @@ public class Startup // v0.7.11 await MigrateSmartFilterEncoding.Migrate(unitOfWork, dataContext, logger); + await MigrateLibrariesToHaveAllFileTypes.Migrate(unitOfWork, dataContext, logger); // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); diff --git a/UI/Web/src/app/_models/auth/member.ts b/UI/Web/src/app/_models/auth/member.ts index 5c9002e9f..31238c68b 100644 --- a/UI/Web/src/app/_models/auth/member.ts +++ b/UI/Web/src/app/_models/auth/member.ts @@ -1,5 +1,5 @@ import { AgeRestriction } from '../metadata/age-restriction'; -import { Library } from '../library'; +import { Library } from '../library/library'; export interface Member { id: number; diff --git a/UI/Web/src/app/_models/library/file-type-group.enum.ts b/UI/Web/src/app/_models/library/file-type-group.enum.ts new file mode 100644 index 000000000..a104782a5 --- /dev/null +++ b/UI/Web/src/app/_models/library/file-type-group.enum.ts @@ -0,0 +1,10 @@ +export enum FileTypeGroup { + Archive = 1, + Epub = 2, + Pdf = 3, + Images = 4 +} + +export const allFileTypeGroup = Object.keys(FileTypeGroup) +.filter(key => !isNaN(Number(key)) && parseInt(key, 10) >= 0) +.map(key => parseInt(key, 10)) as FileTypeGroup[]; diff --git a/UI/Web/src/app/_models/library.ts b/UI/Web/src/app/_models/library/library.ts similarity index 77% rename from UI/Web/src/app/_models/library.ts rename to UI/Web/src/app/_models/library/library.ts index d721231a9..76e463bb4 100644 --- a/UI/Web/src/app/_models/library.ts +++ b/UI/Web/src/app/_models/library/library.ts @@ -1,7 +1,10 @@ +import {FileTypeGroup} from "./file-type-group.enum"; + export enum LibraryType { Manga = 0, Comic = 1, Book = 2, + Images = 3 } export interface Library { @@ -19,4 +22,6 @@ export interface Library { manageReadingLists: boolean; allowScrobbling: boolean; collapseSeriesRelationships: boolean; + libraryFileTypes: Array; + excludePatterns: Array; } diff --git a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts index 26ee5e0bb..fa3a5de99 100644 --- a/UI/Web/src/app/_models/manga-reader/bookmark-info.ts +++ b/UI/Web/src/app/_models/manga-reader/bookmark-info.ts @@ -1,5 +1,5 @@ import { FileDimension } from "src/app/manga-reader/_models/file-dimension"; -import { LibraryType } from "../library"; +import { LibraryType } from "../library/library"; import { MangaFormat } from "../manga-format"; export interface BookmarkInfo { @@ -17,4 +17,4 @@ export interface BookmarkInfo { * This will not always be present. Depends on if asked from backend. */ doublePairs?: {[key: number]: number}; -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_models/reading-list.ts b/UI/Web/src/app/_models/reading-list.ts index daff4f57b..697087274 100644 --- a/UI/Web/src/app/_models/reading-list.ts +++ b/UI/Web/src/app/_models/reading-list.ts @@ -1,4 +1,4 @@ -import { LibraryType } from "./library"; +import { LibraryType } from "./library/library"; import { MangaFormat } from "./manga-format"; export interface ReadingListItem { @@ -27,11 +27,11 @@ export interface ReadingList { coverImageLocked: boolean; items: Array; /** - * If this is empty or null, the cover image isn't set. Do not use this externally. + * If this is empty or null, the cover image isn't set. Do not use this externally. */ coverImage: string; startingYear: number; startingMonth: number; endingYear: number; endingMonth: number; -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_models/recently-added-item.ts b/UI/Web/src/app/_models/recently-added-item.ts index 4c44474a8..b1cc6e8eb 100644 --- a/UI/Web/src/app/_models/recently-added-item.ts +++ b/UI/Web/src/app/_models/recently-added-item.ts @@ -1,4 +1,4 @@ -import { LibraryType } from "./library"; +import { LibraryType } from "./library/library"; export interface RecentlyAddedItem { seriesId: number; @@ -8,6 +8,6 @@ export interface RecentlyAddedItem { libraryId: number; libraryType: LibraryType; volumeId: number; - chapterId: number; + chapterId: number; id: number; // This is UI only, sent from backend but has no relation to any entity -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_models/search/search-result-group.ts b/UI/Web/src/app/_models/search/search-result-group.ts index a9d0005dd..0e5b389f2 100644 --- a/UI/Web/src/app/_models/search/search-result-group.ts +++ b/UI/Web/src/app/_models/search/search-result-group.ts @@ -1,5 +1,5 @@ import { Chapter } from "../chapter"; -import { Library } from "../library"; +import { Library } from "../library/library"; import { MangaFile } from "../manga-file"; import { SearchResult } from "./search-result"; import { Tag } from "../tag"; @@ -24,6 +24,6 @@ export class SearchResultGroup { this.genres = []; this.tags = []; this.files = []; - this.chapters = []; + this.chapters = []; } -} \ No newline at end of file +} diff --git a/UI/Web/src/app/_models/series-group.ts b/UI/Web/src/app/_models/series-group.ts index 657890c25..76c09d135 100644 --- a/UI/Web/src/app/_models/series-group.ts +++ b/UI/Web/src/app/_models/series-group.ts @@ -1,4 +1,4 @@ -import { LibraryType } from "./library"; +import { LibraryType } from "./library/library"; export interface SeriesGroup { seriesId: number; @@ -8,7 +8,7 @@ export interface SeriesGroup { libraryId: number; libraryType: LibraryType; volumeId: number; - chapterId: number; + chapterId: number; id: number; // This is UI only, sent from backend but has no relation to any entity - count: number; -} \ No newline at end of file + count: number; +} diff --git a/UI/Web/src/app/_models/sidenav/sidenav-stream.ts b/UI/Web/src/app/_models/sidenav/sidenav-stream.ts index 7dd672f50..37a2c20e3 100644 --- a/UI/Web/src/app/_models/sidenav/sidenav-stream.ts +++ b/UI/Web/src/app/_models/sidenav/sidenav-stream.ts @@ -1,5 +1,5 @@ import {SideNavStreamType} from "./sidenav-stream-type.enum"; -import {Library, LibraryType} from "../library"; +import {Library, LibraryType} from "../library/library"; import {CommonStream} from "../common-stream"; import {ExternalSource} from "./external-source"; diff --git a/UI/Web/src/app/_pipes/file-type-group.pipe.ts b/UI/Web/src/app/_pipes/file-type-group.pipe.ts new file mode 100644 index 000000000..e996eafde --- /dev/null +++ b/UI/Web/src/app/_pipes/file-type-group.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import {FileTypeGroup} from "../_models/library/file-type-group.enum"; +import {translate} from "@ngneat/transloco"; + +@Pipe({ + name: 'fileTypeGroup', + standalone: true +}) +export class FileTypeGroupPipe implements PipeTransform { + + transform(value: FileTypeGroup): string { + switch (value) { + case FileTypeGroup.Archive: + return translate('file-type-group-pipe.archive'); + case FileTypeGroup.Epub: + return translate('file-type-group-pipe.epub'); + case FileTypeGroup.Pdf: + return translate('file-type-group-pipe.pdf'); + case FileTypeGroup.Images: + return translate('file-type-group-pipe.image'); + + } + } + +} diff --git a/UI/Web/src/app/_pipes/library-type.pipe.ts b/UI/Web/src/app/_pipes/library-type.pipe.ts index 554ce4a15..4686175f4 100644 --- a/UI/Web/src/app/_pipes/library-type.pipe.ts +++ b/UI/Web/src/app/_pipes/library-type.pipe.ts @@ -1,5 +1,5 @@ import {inject, Pipe, PipeTransform} from '@angular/core'; -import { LibraryType } from '../_models/library'; +import { LibraryType } from '../_models/library/library'; import {TranslocoService} from "@ngneat/transloco"; /** diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 75229bd47..15ead6554 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -3,7 +3,7 @@ import { map, Observable, shareReplay } from 'rxjs'; import { Chapter } from '../_models/chapter'; import { CollectionTag } from '../_models/collection-tag'; import { Device } from '../_models/device/device'; -import { Library } from '../_models/library'; +import { Library } from '../_models/library/library'; import { ReadingList } from '../_models/reading-list'; import { Series } from '../_models/series'; import { Volume } from '../_models/volume'; diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 1a6f4082b..580744f43 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -10,7 +10,7 @@ import { ConfirmService } from '../shared/confirm.service'; import { LibrarySettingsModalComponent } from '../sidenav/_modals/library-settings-modal/library-settings-modal.component'; import { Chapter } from '../_models/chapter'; import { Device } from '../_models/device/device'; -import { Library } from '../_models/library'; +import { Library } from '../_models/library/library'; import { ReadingList } from '../_models/reading-list'; import { Series } from '../_models/series'; import { Volume } from '../_models/volume'; diff --git a/UI/Web/src/app/external-source.service.ts b/UI/Web/src/app/_services/external-source.service.ts similarity index 86% rename from UI/Web/src/app/external-source.service.ts rename to UI/Web/src/app/_services/external-source.service.ts index cc561492c..0fd726fa6 100644 --- a/UI/Web/src/app/external-source.service.ts +++ b/UI/Web/src/app/_services/external-source.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; -import {environment} from "../environments/environment"; +import {environment} from "../../environments/environment"; import {HttpClient} from "@angular/common/http"; -import {ExternalSource} from "./_models/sidenav/external-source"; -import {TextResonse} from "./_types/text-response"; +import {ExternalSource} from "../_models/sidenav/external-source"; +import {TextResonse} from "../_types/text-response"; import {map} from "rxjs/operators"; @Injectable({ diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 2d9edd190..176ce9042 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -4,7 +4,7 @@ import { of } from 'rxjs'; import { map } from 'rxjs/operators'; import { environment } from 'src/environments/environment'; import { JumpKey } from '../_models/jumpbar/jump-key'; -import { Library, LibraryType } from '../_models/library'; +import { Library, LibraryType } from '../_models/library/library'; import { DirectoryDto } from '../_models/system/directory-dto'; diff --git a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts index ff23e16ee..6d1ddd105 100644 --- a/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts +++ b/UI/Web/src/app/admin/_modals/library-access-modal/library-access-modal.component.ts @@ -1,6 +1,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; -import {Library} from 'src/app/_models/library'; +import {Library} from 'src/app/_models/library/library'; import {Member} from 'src/app/_models/auth/member'; import {LibraryService} from 'src/app/_services/library.service'; import {SelectionModel} from 'src/app/typeahead/_components/typeahead.component'; diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.ts b/UI/Web/src/app/admin/edit-user/edit-user.component.ts index a0ed23cc1..ba65bae6c 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.ts +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { AgeRestriction } from 'src/app/_models/metadata/age-restriction'; -import { Library } from 'src/app/_models/library'; +import { Library } from 'src/app/_models/library/library'; import { Member } from 'src/app/_models/auth/member'; import { AccountService } from 'src/app/_services/account.service'; import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe'; diff --git a/UI/Web/src/app/admin/invite-user/invite-user.component.ts b/UI/Web/src/app/admin/invite-user/invite-user.component.ts index aaf2b8b12..616697a94 100644 --- a/UI/Web/src/app/admin/invite-user/invite-user.component.ts +++ b/UI/Web/src/app/admin/invite-user/invite-user.component.ts @@ -4,7 +4,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { AgeRestriction } from 'src/app/_models/metadata/age-restriction'; import { InviteUserResponse } from 'src/app/_models/auth/invite-user-response'; -import { Library } from 'src/app/_models/library'; +import { Library } from 'src/app/_models/library/library'; import { AgeRating } from 'src/app/_models/metadata/age-rating'; import { AccountService } from 'src/app/_services/account.service'; import { ApiKeyComponent } from '../../user-settings/api-key/api-key.component'; diff --git a/UI/Web/src/app/admin/library-selector/library-selector.component.ts b/UI/Web/src/app/admin/library-selector/library-selector.component.ts index e3a951dd2..90757b95f 100644 --- a/UI/Web/src/app/admin/library-selector/library-selector.component.ts +++ b/UI/Web/src/app/admin/library-selector/library-selector.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { Library } from 'src/app/_models/library'; +import { Library } from 'src/app/_models/library/library'; import { Member } from 'src/app/_models/auth/member'; import { LibraryService } from 'src/app/_services/library.service'; import { SelectionModel } from 'src/app/typeahead/_components/typeahead.component'; diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index 625e011d4..e31dbb0ff 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -13,7 +13,7 @@ import { ConfirmService } from 'src/app/shared/confirm.service'; import { LibrarySettingsModalComponent } from 'src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component'; import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event'; import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event'; -import { Library } from 'src/app/_models/library'; +import { Library } from 'src/app/_models/library/library'; import { LibraryService } from 'src/app/_services/library.service'; import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index bcf50bb26..949143fb4 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -34,7 +34,7 @@ import { ReadingDirection } from 'src/app/_models/preferences/reading-direction' import {WritingStyle} from "../../../_models/preferences/writing-style"; import { MangaFormat } from 'src/app/_models/manga-format'; import { LibraryService } from 'src/app/_services/library.service'; -import { LibraryType } from 'src/app/_models/library'; +import { LibraryType } from 'src/app/_models/library/library'; import { BookTheme } from 'src/app/_models/preferences/book-theme'; import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mode'; import { PageStyle, ReaderSettingsComponent } from '../reader-settings/reader-settings.component'; diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html index 576fe6948..0a66a93be 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.html @@ -351,24 +351,7 @@ {{t(tabs[TabID.WebLinks])}}

{{t('web-link-description')}}

-
-
-
- - -
-
-
- - -
-
+
diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 96b896d78..379b68521 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -54,6 +54,7 @@ import {DefaultValuePipe} from "../../../_pipes/default-value.pipe"; import {TranslocoModule} from "@ngneat/transloco"; import {TranslocoDatePipe} from "@ngneat/transloco-locale"; import {UtcToLocalTimePipe} from "../../../_pipes/utc-to-local-time.pipe"; +import {EditListComponent} from "../../../shared/edit-list/edit-list.component"; enum TabID { General = 0, @@ -93,6 +94,7 @@ enum TabID { TranslocoModule, TranslocoDatePipe, UtcToLocalTimePipe, + EditListComponent, ], templateUrl: './edit-series-modal.component.html', styleUrls: ['./edit-series-modal.component.scss'], @@ -100,7 +102,24 @@ enum TabID { }) export class EditSeriesModalComponent implements OnInit { + public readonly modal = inject(NgbActiveModal); + private readonly seriesService = inject(SeriesService); + public readonly utilityService = inject(UtilityService); + private readonly fb = inject(FormBuilder); + public readonly imageService = inject(ImageService); + private readonly libraryService = inject(LibraryService); + private readonly collectionService = inject(CollectionTagService); + private readonly uploadService = inject(UploadService); + private readonly metadataService = inject(MetadataService); + private readonly cdRef = inject(ChangeDetectorRef); + + protected readonly TabID = TabID; + protected readonly PersonRole = PersonRole; + protected readonly Breakpoint = Breakpoint; + @Input({required: true}) series!: Series; + + seriesVolumes: any[] = []; isLoadingVolumes = false; /** @@ -140,18 +159,6 @@ export class EditSeriesModalComponent implements OnInit { saveNestedComponents: EventEmitter = new EventEmitter(); - get Breakpoint(): typeof Breakpoint { - return Breakpoint; - } - - get PersonRole() { - return PersonRole; - } - - get TabID(): typeof TabID { - return TabID; - } - get WebLinks() { return this.metadata?.webLinks.split(',') || ['']; } @@ -160,17 +167,6 @@ export class EditSeriesModalComponent implements OnInit { return this.peopleSettings[role]; } - constructor(public modal: NgbActiveModal, - private seriesService: SeriesService, - public utilityService: UtilityService, - private fb: FormBuilder, - public imageService: ImageService, - private libraryService: LibraryService, - private collectionService: CollectionTagService, - private uploadService: UploadService, - private metadataService: MetadataService, - private readonly cdRef: ChangeDetectorRef) { } - ngOnInit(): void { this.imageUrls.push(this.imageService.getSeriesCoverImage(this.series.id)); @@ -225,10 +221,6 @@ export class EditSeriesModalComponent implements OnInit { this.editSeriesForm.get('language')?.patchValue(this.metadata.language); this.editSeriesForm.get('releaseYear')?.patchValue(this.metadata.releaseYear); - this.WebLinks.forEach((link, index) => { - this.editSeriesForm.addControl('link' + index, new FormControl(link, [])); - }); - this.cdRef.markForCheck(); this.editSeriesForm.get('name')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => { @@ -416,8 +408,8 @@ export class EditSeriesModalComponent implements OnInit { if (presetField && presetField.length > 0) { const fetch = personSettings.fetchFn as ((filter: string) => Observable); return fetch('').pipe(map(people => { - const persetIds = presetField.map(p => p.id); - personSettings.savedData = people.filter(person => persetIds.includes(person.id)); + const presetIds = presetField.map(p => p.id); + personSettings.savedData = people.filter(person => presetIds.includes(person.id)); this.peopleSettings[role] = personSettings; this.updatePerson(personSettings.savedData as Person[], role); return true; @@ -521,23 +513,14 @@ export class EditSeriesModalComponent implements OnInit { return this.collectionService.search(filter); } - formatChapterNumber(chapter: Chapter) { - if (chapter.number === '0') { - return '1'; - } - return chapter.number; + updateWeblinks(items: Array) { + this.metadata.webLinks = items.map(s => s.replaceAll(',', '%2C')).join(','); } + save() { const model = this.editSeriesForm.value; const selectedIndex = this.editSeriesForm.get('coverImageIndex')?.value || 0; - this.metadata.webLinks = Object.keys(this.editSeriesForm.controls) - .filter(key => key.startsWith('link')) - .map(key => this.editSeriesForm.get(key)?.value.replace(',', '%2C')) - .filter(v => v !== null && v !== '') - .join(','); - - const apis = [ this.seriesService.updateMetadata(this.metadata, this.collectionTags) @@ -566,21 +549,6 @@ export class EditSeriesModalComponent implements OnInit { }); } - addWebLink() { - this.metadata.webLinks += ','; - this.editSeriesForm.addControl('link' + (this.WebLinks.length - 1), new FormControl('', [])); - this.cdRef.markForCheck(); - } - - removeWebLink(index: number) { - const tokens = this.metadata.webLinks.split(','); - const tokenToRemove = tokens[index]; - - this.metadata.webLinks = tokens.filter(t => t != tokenToRemove).join(','); - this.editSeriesForm.removeControl('link' + index, {emitEvent: true}); - this.cdRef.markForCheck(); - } - updateCollections(tags: CollectionTag[]) { this.collectionTags = tags; this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index 302de4c33..83e48cfc7 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -23,7 +23,7 @@ import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.ser import { Chapter } from 'src/app/_models/chapter'; import { ChapterMetadata } from 'src/app/_models/metadata/chapter-metadata'; import { Device } from 'src/app/_models/device/device'; -import { LibraryType } from 'src/app/_models/library'; +import { LibraryType } from 'src/app/_models/library/library'; import { MangaFile } from 'src/app/_models/manga-file'; import { MangaFormat } from 'src/app/_models/manga-format'; import { Volume } from 'src/app/_models/volume'; diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index de3507e18..9ff7aa4fe 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -25,7 +25,7 @@ import {FilterSettings} from 'src/app/metadata-filter/filter-settings'; import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; import {JumpKey} from 'src/app/_models/jumpbar/jump-key'; -import {Library} from 'src/app/_models/library'; +import {Library} from 'src/app/_models/library/library'; import {Pagination} from 'src/app/_models/pagination'; import {FilterEvent, FilterItem, SortField} from 'src/app/_models/metadata/series-filter'; import {ActionItem} from 'src/app/_services/action-factory.service'; diff --git a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts index 4bcab6708..23a466450 100644 --- a/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts +++ b/UI/Web/src/app/cards/entity-info-cards/entity-info-cards.component.ts @@ -10,7 +10,7 @@ import { UtilityService } from 'src/app/shared/_services/utility.service'; import { Chapter } from 'src/app/_models/chapter'; import { ChapterMetadata } from 'src/app/_models/metadata/chapter-metadata'; import { HourEstimateRange } from 'src/app/_models/series-detail/hour-estimate-range'; -import { LibraryType } from 'src/app/_models/library'; +import { LibraryType } from 'src/app/_models/library/library'; import { MangaFormat } from 'src/app/_models/manga-format'; import { AgeRating } from 'src/app/_models/metadata/age-rating'; import { Volume } from 'src/app/_models/volume'; diff --git a/UI/Web/src/app/cards/entity-title/entity-title.component.ts b/UI/Web/src/app/cards/entity-title/entity-title.component.ts index ebfbc162f..13c7f4bb8 100644 --- a/UI/Web/src/app/cards/entity-title/entity-title.component.ts +++ b/UI/Web/src/app/cards/entity-title/entity-title.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { UtilityService } from 'src/app/shared/_services/utility.service'; import { Chapter } from 'src/app/_models/chapter'; -import { LibraryType } from 'src/app/_models/library'; +import { LibraryType } from 'src/app/_models/library/library'; import { Volume } from 'src/app/_models/volume'; import {CommonModule, NgSwitch} from "@angular/common"; import {TranslocoModule} from "@ngneat/transloco"; diff --git a/UI/Web/src/app/cards/list-item/list-item.component.ts b/UI/Web/src/app/cards/list-item/list-item.component.ts index 3bafbaf6c..b2dd10473 100644 --- a/UI/Web/src/app/cards/list-item/list-item.component.ts +++ b/UI/Web/src/app/cards/list-item/list-item.component.ts @@ -14,7 +14,7 @@ import { Download } from 'src/app/shared/_models/download'; import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; import { Chapter } from 'src/app/_models/chapter'; -import { LibraryType } from 'src/app/_models/library'; +import { LibraryType } from 'src/app/_models/library/library'; import { RelationKind } from 'src/app/_models/series-detail/relation-kind'; import { Volume } from 'src/app/_models/volume'; import { Action, ActionItem } from 'src/app/_services/action-factory.service'; diff --git a/UI/Web/src/app/dashboard/_components/dashboard.component.ts b/UI/Web/src/app/dashboard/_components/dashboard.component.ts index dbe4945b3..43b46d55c 100644 --- a/UI/Web/src/app/dashboard/_components/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/_components/dashboard.component.ts @@ -4,7 +4,7 @@ import {Router, RouterLink} from '@angular/router'; import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs'; import {debounceTime, map, shareReplay, take, tap, throttleTime} from 'rxjs/operators'; import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service'; -import {Library} from 'src/app/_models/library'; +import {Library} from 'src/app/_models/library/library'; import {RecentlyAddedItem} from 'src/app/_models/recently-added-item'; import {SortField} from 'src/app/_models/metadata/series-filter'; import {AccountService} from 'src/app/_services/account.service'; diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index a3c7b1b5e..635b2a3cd 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -14,7 +14,7 @@ import {take} from 'rxjs/operators'; import {BulkSelectionService} from '../cards/bulk-selection.service'; import {KEY_CODES, UtilityService} from '../shared/_services/utility.service'; import {SeriesAddedEvent} from '../_models/events/series-added-event'; -import {Library} from '../_models/library'; +import {Library} from '../_models/library/library'; import {Pagination} from '../_models/pagination'; import {Series} from '../_models/series'; import {FilterEvent} from '../_models/metadata/series-filter'; diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index eca3b6421..56e1bb397 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -37,7 +37,7 @@ import {ToastrService} from 'ngx-toastr'; import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component'; import {Stack} from 'src/app/shared/data-structures/stack'; import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service'; -import {LibraryType} from 'src/app/_models/library'; +import {LibraryType} from 'src/app/_models/library/library'; import {MangaFormat} from 'src/app/_models/manga-format'; import {PageSplitOption} from 'src/app/_models/preferences/page-split-option'; import {layoutModes, pageSplitOptions} from 'src/app/_models/preferences/preferences'; diff --git a/UI/Web/src/app/manga-reader/_models/chapter-info.ts b/UI/Web/src/app/manga-reader/_models/chapter-info.ts index e3447d60d..3d9f4bcf2 100644 --- a/UI/Web/src/app/manga-reader/_models/chapter-info.ts +++ b/UI/Web/src/app/manga-reader/_models/chapter-info.ts @@ -1,4 +1,4 @@ -import { LibraryType } from "src/app/_models/library"; +import { LibraryType } from "src/app/_models/library/library"; import { MangaFormat } from "src/app/_models/manga-format"; import { FileDimension } from "./file-dimension"; diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index 75e6beabb..bff38701e 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -13,7 +13,7 @@ import { import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {NgbCollapse, NgbModal, NgbRating, NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {Breakpoint, UtilityService} from '../shared/_services/utility.service'; -import {Library} from '../_models/library'; +import {Library} from '../_models/library/library'; import {allSortFields, FilterEvent, FilterItem, SortField} from '../_models/metadata/series-filter'; import {ToggleService} from '../_services/toggle.service'; import {FilterSettings} from './filter-settings'; diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts index 14ff581cb..34c2170dc 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.ts @@ -15,7 +15,7 @@ import {fromEvent} from 'rxjs'; import {debounceTime, distinctUntilChanged, filter, tap} from 'rxjs/operators'; import {Chapter} from 'src/app/_models/chapter'; import {CollectionTag} from 'src/app/_models/collection-tag'; -import {Library} from 'src/app/_models/library'; +import {Library} from 'src/app/_models/library/library'; import {MangaFile} from 'src/app/_models/manga-file'; import {PersonRole} from 'src/app/_models/metadata/person'; import {ReadingList} from 'src/app/_models/reading-list'; 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 3d5158b93..fa9073380 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 @@ -4,7 +4,7 @@ import {ToastrService} from 'ngx-toastr'; import {take} from 'rxjs/operators'; import {ConfirmService} from 'src/app/shared/confirm.service'; import {UtilityService} from 'src/app/shared/_services/utility.service'; -import {LibraryType} from 'src/app/_models/library'; +import {LibraryType} from 'src/app/_models/library/library'; import {MangaFormat} from 'src/app/_models/manga-format'; import {ReadingList, ReadingListItem} from 'src/app/_models/reading-list'; import {AccountService} from 'src/app/_services/account.service'; diff --git a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts index 0caa0ded6..885a1a2ef 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-item/reading-list-item.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { LibraryType } from 'src/app/_models/library'; +import { LibraryType } from 'src/app/_models/library/library'; import { MangaFormat } from 'src/app/_models/manga-format'; import { ReadingListItem } from 'src/app/_models/reading-list'; import { ImageService } from 'src/app/_services/image.service'; diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts index 18e6586cb..9db60805d 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.ts @@ -14,7 +14,7 @@ import {ProviderImagePipe} from "../../../_pipes/provider-image.pipe"; import {NgbPopover, NgbRating} from "@ng-bootstrap/ng-bootstrap"; import {LoadingComponent} from "../../../shared/loading/loading.component"; import {AccountService} from "../../../_services/account.service"; -import {LibraryType} from "../../../_models/library"; +import {LibraryType} from "../../../_models/library/library"; import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe"; import {NgxStarsModule} from "ngx-stars"; import {ThemeService} from "../../../_services/theme.service"; 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 0475bff10..267f5e09b 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 @@ -141,6 +141,7 @@
+ {{item.id}} 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 9d01b60e1..dcde15d0a 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 @@ -44,7 +44,7 @@ import {Chapter} from 'src/app/_models/chapter'; import {Device} from 'src/app/_models/device/device'; import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event'; import {SeriesRemovedEvent} from 'src/app/_models/events/series-removed-event'; -import {LibraryType} from 'src/app/_models/library'; +import {LibraryType} from 'src/app/_models/library/library'; import {ReadingList} from 'src/app/_models/reading-list'; import {Series} from 'src/app/_models/series'; import {RelatedSeries} from 'src/app/_models/series-detail/related-series'; diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index e186f622c..374c866a6 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -27,7 +27,7 @@ import {A11yClickDirective} from "../../../shared/a11y-click.directive"; import {PersonBadgeComponent} from "../../../shared/person-badge/person-badge.component"; import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap"; import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component"; -import {LibraryType} from "../../../_models/library"; +import {LibraryType} from "../../../_models/library/library"; import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component"; import {TranslocoDirective} from "@ngneat/transloco"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; diff --git a/UI/Web/src/app/shared/_services/utility.service.ts b/UI/Web/src/app/shared/_services/utility.service.ts index 3dbb6f3ba..8c2382cb5 100644 --- a/UI/Web/src/app/shared/_services/utility.service.ts +++ b/UI/Web/src/app/shared/_services/utility.service.ts @@ -1,7 +1,7 @@ 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'; +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'; @@ -70,6 +70,7 @@ export class UtilityService { return this.translocoService.translate('common.issue-hash-num'); } return this.translocoService.translate('common.issue-num') + (includeSpace ? ' ' : ''); + case LibraryType.Images: case LibraryType.Manga: return this.translocoService.translate('common.chapter-num') + (includeSpace ? ' ' : ''); } 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 new file mode 100644 index 000000000..7245422d9 --- /dev/null +++ b/UI/Web/src/app/shared/edit-list/edit-list.component.html @@ -0,0 +1,24 @@ +
+ + @for(item of Items; let i = $index; track item) { +
+
+
+ + +
+
+
+ + +
+
+ } + +
diff --git a/UI/Web/src/app/shared/edit-list/edit-list.component.scss b/UI/Web/src/app/shared/edit-list/edit-list.component.scss new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..09d10ffa4 --- /dev/null +++ b/UI/Web/src/app/shared/edit-list/edit-list.component.ts @@ -0,0 +1,82 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, DestroyRef, + EventEmitter, + inject, + Input, + OnInit, + Output +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {Select2Module} from "ng-select2-component"; +import {TranslocoDirective} from "@ngneat/transloco"; +import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; +import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators"; + +@Component({ + selector: 'app-edit-list', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, Select2Module, TranslocoDirective], + templateUrl: './edit-list.component.html', + styleUrl: './edit-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EditListComponent implements OnInit { + + private readonly cdRef = inject(ChangeDetectorRef); + private readonly destroyRef = inject(DestroyRef); + + @Input({required: true}) items: Array = []; + @Input({required: true}) label = ''; + @Output() updateItems = new EventEmitter>(); + + form: FormGroup = new FormGroup({}); + private combinedItems: string = ''; + + get Items() { + return this.combinedItems.split(',') || ['']; + } + + + ngOnInit() { + this.items.forEach((link, index) => { + this.form.addControl('link' + index, new FormControl(link, [])); + }); + + this.combinedItems = this.items.join(','); + + this.form.valueChanges.pipe( + debounceTime(100), + distinctUntilChanged(), + tap(data => this.emit()), + takeUntilDestroyed(this.destroyRef)) + .subscribe(); + this.cdRef.markForCheck(); + } + + add() { + this.combinedItems += ','; + this.form.addControl('link' + (this.Items.length - 1), new FormControl('', [])); + this.emit(); + this.cdRef.markForCheck(); + } + + remove(index: number) { + const tokens = this.combinedItems.split(','); + const tokenToRemove = tokens[index]; + + this.combinedItems = tokens.filter(t => t != tokenToRemove).join(','); + this.form.removeControl('link' + index, {emitEvent: true}); + this.emit(); + this.cdRef.markForCheck(); + } + + emit() { + this.updateItems.emit(Object.keys(this.form.controls) + .filter(key => key.startsWith('link')) + .map(key => this.form.get(key)?.value) + .filter(v => v !== null && v !== '')); + } +} diff --git a/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts b/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts index 90c2028bd..5d232c152 100644 --- a/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts +++ b/UI/Web/src/app/sidenav/_components/customize-sidenav-streams/customize-sidenav-streams.component.ts @@ -21,7 +21,7 @@ import {NavService} from "../../../_services/nav.service"; import {DashboardStreamListItemComponent} from "../dashboard-stream-list-item/dashboard-stream-list-item.component"; import {TranslocoDirective} from "@ngneat/transloco"; import {SidenavStreamListItemComponent} from "../sidenav-stream-list-item/sidenav-stream-list-item.component"; -import {ExternalSourceService} from "../../../external-source.service"; +import {ExternalSourceService} from "../../../_services/external-source.service"; import {ExternalSource} from "../../../_models/sidenav/external-source"; import {SideNavStreamType} from "../../../_models/sidenav/sidenav-stream-type.enum"; import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms"; diff --git a/UI/Web/src/app/sidenav/_components/edit-external-source-item/edit-external-source-item.component.ts b/UI/Web/src/app/sidenav/_components/edit-external-source-item/edit-external-source-item.component.ts index 20b01e131..7f3734a11 100644 --- a/UI/Web/src/app/sidenav/_components/edit-external-source-item/edit-external-source-item.component.ts +++ b/UI/Web/src/app/sidenav/_components/edit-external-source-item/edit-external-source-item.component.ts @@ -4,7 +4,7 @@ import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/ import {ExternalSource} from "../../../_models/sidenav/external-source"; import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap"; import {translate, TranslocoDirective} from "@ngneat/transloco"; -import {ExternalSourceService} from "../../../external-source.service"; +import {ExternalSourceService} from "../../../_services/external-source.service"; import {distinctUntilChanged, filter, tap} from "rxjs/operators"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {switchMap} from "rxjs"; diff --git a/UI/Web/src/app/sidenav/_components/manage-external-sources/manage-external-sources.component.ts b/UI/Web/src/app/sidenav/_components/manage-external-sources/manage-external-sources.component.ts index 4f01afb11..ec53e2831 100644 --- a/UI/Web/src/app/sidenav/_components/manage-external-sources/manage-external-sources.component.ts +++ b/UI/Web/src/app/sidenav/_components/manage-external-sources/manage-external-sources.component.ts @@ -7,7 +7,7 @@ import {AccountService} from "../../../_services/account.service"; import {ToastrService} from "ngx-toastr"; import {EditExternalSourceItemComponent} from "../edit-external-source-item/edit-external-source-item.component"; import {ExternalSource} from "../../../_models/sidenav/external-source"; -import {ExternalSourceService} from "../../../external-source.service"; +import {ExternalSourceService} from "../../../_services/external-source.service"; import {FilterPipe} from "../../../_pipes/filter.pipe"; import {SmartFilter} from "../../../_models/metadata/v2/smart-filter"; 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 cdde7ce05..53eed71e0 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 @@ -13,7 +13,7 @@ import { ImportCblModalComponent } from 'src/app/reading-list/_modals/import-cbl import { ImageService } from 'src/app/_services/image.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { Breakpoint, UtilityService } from '../../../shared/_services/utility.service'; -import { Library, LibraryType } from '../../../_models/library'; +import { Library, LibraryType } from '../../../_models/library/library'; import { AccountService } from '../../../_services/account.service'; import { Action, ActionFactoryService, ActionItem } from '../../../_services/action-factory.service'; import { ActionService } from '../../../_services/action.service'; @@ -186,6 +186,8 @@ export class SideNavComponent implements OnInit { case LibraryType.Comic: case LibraryType.Manga: return 'fa-book-open'; + case LibraryType.Images: + return 'fa-images'; } } diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 847f861d0..7299e4add 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -88,6 +88,48 @@
  • {{t(TabID.Advanced)}} +
    +
    +
    +
    {{t('file-type-group-label')}}
    +

    + {{t('file-type-group-tooltip')}} +

    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    + +

    +
    +
    + + {{t('exclude-patterns-tooltip')}} + {{t('help')}} + + +
    +
    +
    +
    + +
    + +
    + + +
    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 e9969dc33..9695465ea 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 @@ -1,6 +1,18 @@ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + DestroyRef, + inject, + Input, + OnInit +} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import { + NgbAccordionBody, + NgbAccordionButton, NgbAccordionCollapse, + NgbAccordionDirective, NgbAccordionHeader, NgbAccordionItem, NgbActiveModal, NgbModal, NgbModalModule, @@ -20,7 +32,7 @@ import { } from 'src/app/admin/_modals/directory-picker/directory-picker.component'; import {ConfirmService} from 'src/app/shared/confirm.service'; import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service'; -import {Library, LibraryType} from 'src/app/_models/library'; +import {Library, LibraryType} from 'src/app/_models/library/library'; import {ImageService} from 'src/app/_services/image.service'; import {LibraryService} from 'src/app/_services/library.service'; import {UploadService} from 'src/app/_services/upload.service'; @@ -30,6 +42,9 @@ import {SentenceCasePipe} from "../../../_pipes/sentence-case.pipe"; import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component"; import {translate, TranslocoModule} from "@ngneat/transloco"; import {DefaultDatePipe} from "../../../_pipes/default-date.pipe"; +import {allFileTypeGroup, FileTypeGroup} from "../../../_models/library/file-type-group.enum"; +import {FileTypeGroupPipe} from "../../../_pipes/file-type-group.pipe"; +import {EditListComponent} from "../../../shared/edit-list/edit-list.component"; enum TabID { General = 'general-tab', @@ -48,7 +63,9 @@ enum StepID { @Component({ selector: 'app-library-settings-modal', standalone: true, - imports: [CommonModule, NgbModalModule, NgbNavLink, NgbNavItem, NgbNavContent, ReactiveFormsModule, NgbTooltip, SentenceCasePipe, NgbNav, NgbNavOutlet, CoverImageChooserComponent, TranslocoModule, DefaultDatePipe], + imports: [CommonModule, NgbModalModule, NgbNavLink, NgbNavItem, NgbNavContent, ReactiveFormsModule, NgbTooltip, + SentenceCasePipe, NgbNav, NgbNavOutlet, CoverImageChooserComponent, TranslocoModule, DefaultDatePipe, + FileTypeGroupPipe, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionButton, NgbAccordionCollapse, NgbAccordionBody, EditListComponent], templateUrl: './library-settings-modal.component.html', styleUrls: ['./library-settings-modal.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -81,23 +98,24 @@ export class LibrarySettingsModalComponent implements OnInit { isAddLibrary = false; setupStep = StepID.General; + fileTypeGroups = allFileTypeGroup; + excludePatterns: Array = ['']; protected readonly Breakpoint = Breakpoint; protected readonly TabID = TabID; + constructor(public utilityService: UtilityService, private uploadService: UploadService, private modalService: NgbModal, private settingService: SettingsService, public modal: NgbActiveModal, private confirmService: ConfirmService, private libraryService: LibraryService, private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef, private imageService: ImageService) { } ngOnInit(): void { - this.settingService.getLibraryTypes().subscribe((types) => { this.libraryTypes = types; this.cdRef.markForCheck(); }); - if (this.library === undefined) { this.isAddLibrary = true; this.cdRef.markForCheck(); @@ -113,7 +131,6 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('allowScrobbling')?.disable(); } - this.libraryForm.get('name')?.valueChanges.pipe( debounceTime(100), distinctUntilChanged(), @@ -132,6 +149,38 @@ export class LibrarySettingsModalComponent implements OnInit { this.setValues(); + + // This needs to only apply after first render + this.libraryForm.get('type')?.valueChanges.pipe( + tap((type: LibraryType) => { + switch (type) { + case LibraryType.Manga: + this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false); + break; + case LibraryType.Comic: + this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false); + break; + case LibraryType.Book: + this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(true); + break; + case LibraryType.Images: + this.libraryForm.get(FileTypeGroup.Archive + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Images + '')?.setValue(true); + this.libraryForm.get(FileTypeGroup.Pdf + '')?.setValue(false); + this.libraryForm.get(FileTypeGroup.Epub + '')?.setValue(false); + } + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); } setValues() { @@ -148,8 +197,27 @@ export class LibrarySettingsModalComponent implements OnInit { this.libraryForm.get('allowScrobbling')?.setValue(this.library.allowScrobbling); this.selectedFolders = this.library.folders; this.madeChanges = false; - this.cdRef.markForCheck(); + for(let fileTypeGroup of allFileTypeGroup) { + this.libraryForm.addControl(fileTypeGroup + '', new FormControl(this.library.libraryFileTypes.includes(fileTypeGroup), [])); + } + for(let glob of this.library.excludePatterns) { + this.libraryForm.addControl('excludeGlob-' , new FormControl(glob, [])); + } + } else { + for(let fileTypeGroup of allFileTypeGroup) { + this.libraryForm.addControl(fileTypeGroup + '', new FormControl(true, [])); + } } + this.excludePatterns = this.library.excludePatterns; + if (this.excludePatterns.length === 0) { + this.excludePatterns = ['']; + } + this.cdRef.markForCheck(); + } + + updateGlobs(items: Array) { + this.excludePatterns = items; + this.cdRef.markForCheck(); } isDisabled() { @@ -172,6 +240,13 @@ export class LibrarySettingsModalComponent implements OnInit { async save() { const model = this.libraryForm.value; model.folders = this.selectedFolders; + model.fileGroupTypes = []; + for(let fileTypeGroup of allFileTypeGroup) { + if (model[fileTypeGroup]) { + model.fileGroupTypes.push(fileTypeGroup); + } + } + model.excludePatterns = this.excludePatterns; if (this.libraryForm.errors) { return; diff --git a/UI/Web/src/app/statistics/_models/server-statistics.ts b/UI/Web/src/app/statistics/_models/server-statistics.ts index e46028102..67f906498 100644 --- a/UI/Web/src/app/statistics/_models/server-statistics.ts +++ b/UI/Web/src/app/statistics/_models/server-statistics.ts @@ -1,4 +1,4 @@ -import { Library } from "src/app/_models/library"; +import { Library } from "src/app/_models/library/library"; import { Series } from "src/app/_models/series"; import { User } from "src/app/_models/user"; import { StatCount } from "./stat-count"; @@ -17,4 +17,4 @@ export interface ServerStatistics { mostActiveLibraries: Array>; mostReadSeries: Array>; recentlyRead: Array; -} \ No newline at end of file +} diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 1ecab674f..16b27ff73 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -813,7 +813,19 @@ "cancel": "{{common.cancel}}", "next": "Next", "save": "{{common.save}}", - "required-field": "{{validation.required-field}}" + "required-field": "{{validation.required-field}}", + "file-type-group-label": "File Types", + "file-type-group-tooltip": "What types of files should Kavita scan for. For example, Archive will include all cb*, zip, rar, etc files.", + "exclude-patterns-label": "Exclude Patterns", + "exclude-patterns-tooltip": "Configure a set of patterns (Glob syntax) that Kavita will match when scanning directories and exclude from Scanner results.", + "help": "{{common.help}}" + }, + + "file-type-group-pipe": { + "archive": "Archive", + "epub": "Epub", + "pdf": "Pdf", + "image": "Image" }, "reader-settings": { @@ -944,7 +956,7 @@ "description-part-2": "wiki for hints.", "target-series": "Target Series", "relationship": "Relationship", - "remove": "Remove", + "remove": "{{common.remove}}", "add-relationship": "Add Relationship", "parent": "{{relationship-pipe.parent}}" }, @@ -1414,7 +1426,7 @@ "scroll-to-top-alt": "Scroll to Top", "server-settings": "Server Settings", "settings": "Settings", - "help": "Help", + "help": "{{common.help}}", "announcements": "Announcements", "logout": "Logout", "all-filters": "Smart Filters" @@ -1615,8 +1627,6 @@ "release-year-label": "Release Year", "web-link-description": "Here you can add many different links to external services.", "web-link-label": "Web Link", - "add-link-alt": "Add Link", - "remove-link-alt": "Remove Link", "cover-image-description": "Upload and choose a new cover image. Press Save to upload and override the cover.", "save": "{{common.save}}", "field-locked-alt": "Field is locked", @@ -2059,6 +2069,7 @@ "add": "Add", "apply": "Apply", "delete": "Delete", + "remove": "Remove", "edit": "Edit", "help": "Help", "submit": "Submit", diff --git a/openapi.json b/openapi.json index 5d6d0d2c0..9dfbe9366 100644 --- a/openapi.json +++ b/openapi.json @@ -15537,6 +15537,20 @@ "$ref": "#/components/schemas/Series" }, "nullable": true + }, + "libraryFileTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryFileTypeGroup" + }, + "nullable": true + }, + "libraryExcludePatterns": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LibraryExcludePattern" + }, + "nullable": true } }, "additionalProperties": false @@ -15610,6 +15624,30 @@ "collapseSeriesRelationships": { "type": "boolean", "description": "When showing series, only parent series or series with no relationships will be returned" + }, + "libraryFileTypes": { + "type": "array", + "items": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "description": "Represents a set of file types that can be scanned", + "format": "int32" + }, + "description": "The types of file type groups the library will scan for", + "nullable": true + }, + "excludePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A set of globs that will exclude matching content from being scanned", + "nullable": true } }, "additionalProperties": false @@ -15627,6 +15665,55 @@ }, "additionalProperties": false }, + "LibraryExcludePattern": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "pattern": { + "type": "string", + "nullable": true + }, + "libraryId": { + "type": "integer", + "format": "int32" + }, + "library": { + "$ref": "#/components/schemas/Library" + } + }, + "additionalProperties": false + }, + "LibraryFileTypeGroup": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "fileTypeGroup": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "description": "Represents a set of file types that can be scanned", + "format": "int32" + }, + "libraryId": { + "type": "integer", + "format": "int32" + }, + "library": { + "$ref": "#/components/schemas/Library" + } + }, + "additionalProperties": false + }, "LoginDto": { "type": "object", "properties": { @@ -18779,6 +18866,7 @@ "UpdateLibraryDto": { "required": [ "allowScrobbling", + "fileGroupTypes", "folders", "folderWatching", "id", @@ -18836,6 +18924,29 @@ }, "allowScrobbling": { "type": "boolean" + }, + "fileGroupTypes": { + "type": "array", + "items": { + "enum": [ + 1, + 2, + 3, + 4 + ], + "type": "integer", + "description": "Represents a set of file types that can be scanned", + "format": "int32" + }, + "description": "What types of files to allow the scanner to pickup" + }, + "excludePatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A set of Glob patterns that the scanner will exclude processing", + "nullable": true } }, "additionalProperties": false