mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
.kavitaignore no more (#2442)
This commit is contained in:
parent
cd27efecdd
commit
7221501c4d
@ -879,7 +879,7 @@ public class DirectoryServiceTests
|
||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), 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<ILogger<DirectoryService>>(), 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<ILogger<DirectoryService>>(), 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<ILogger<DirectoryService>>(), 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);
|
||||
|
||||
|
@ -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<string>() {"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<string>() {"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<IEventHub>());
|
||||
|
||||
var directoriesSeen = new HashSet<string>();
|
||||
var library =
|
||||
await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(1,
|
||||
LibraryIncludes.Folders | LibraryIncludes.FileTypes);
|
||||
await psf.ProcessFiles("C:/Data/", true, await _unitOfWork.SeriesRepository.GetFolderPathMap(1),
|
||||
(files, directoryPath) =>
|
||||
{
|
||||
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<IEventHub>());
|
||||
|
||||
var directoriesSeen = new HashSet<string>();
|
||||
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);
|
||||
}
|
||||
|
@ -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<ActionResult> 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)
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public bool CollapseSeriesRelationships { get; set; } = false;
|
||||
/// <summary>
|
||||
/// The types of file type groups the library will scan for
|
||||
/// </summary>
|
||||
public ICollection<FileTypeGroup> LibraryFileTypes { get; set; }
|
||||
/// <summary>
|
||||
/// A set of globs that will exclude matching content from being scanned
|
||||
/// </summary>
|
||||
public ICollection<string> ExcludePatterns { get; set; }
|
||||
}
|
||||
|
@ -28,4 +28,13 @@ public class UpdateLibraryDto
|
||||
public bool ManageReadingLists { get; init; }
|
||||
[Required]
|
||||
public bool AllowScrobbling { get; init; }
|
||||
/// <summary>
|
||||
/// What types of files to allow the scanner to pickup
|
||||
/// </summary>
|
||||
[Required]
|
||||
public ICollection<FileTypeGroup> FileGroupTypes { get; init; }
|
||||
/// <summary>
|
||||
/// A set of Glob patterns that the scanner will exclude processing
|
||||
/// </summary>
|
||||
public ICollection<string> ExcludePatterns { get; init; }
|
||||
}
|
||||
|
@ -1,136 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class MigrateBrokenGMT1Dates
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Constants;
|
||||
using API.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.5.1. Adds the role to all users.
|
||||
/// </summary>
|
||||
public static class MigrateChangePasswordRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Will not run if any users have the ChangePassword role already
|
||||
/// </summary>
|
||||
/// <param name="unitOfWork"></param>
|
||||
/// <param name="userManager"></param>
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, UserManager<AppUser> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.6. Adds the role to all users.
|
||||
/// </summary>
|
||||
public static class MigrateChangeRestrictionRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Will not run if any users have the <see cref="PolicyConstants.ChangeRestrictionRole"/> role already
|
||||
/// </summary>
|
||||
/// <param name="unitOfWork"></param>
|
||||
/// <param name="userManager"></param>
|
||||
/// <param name="logger"></param>
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, UserManager<AppUser> userManager, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class MigrateDashboardStreamNamesToLocaleKeys
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// v0.7.4 introduced Scrobbling with Kavita+. By default, it is on, but Comic libraries have no scrobble providers, so disable
|
||||
/// </summary>
|
||||
public static class MigrateDisableScrobblingOnComicLibraries
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.5.6 and v0.7.6, Ratings > 0 need to have "HasRatingSet"
|
||||
/// </summary>
|
||||
/// <remarks>Added in v0.7.5.6</remarks>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class MigrateExistingRatings
|
||||
{
|
||||
public static async Task Migrate(DataContext context, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.11 with the removal of .Kavitaignore files
|
||||
/// </summary>
|
||||
public static class MigrateLibrariesToHaveAllFileTypes
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Added in v0.7.1.18
|
||||
/// </summary>
|
||||
public static class MigrateLoginRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// Will not run if any users have the <see cref="PolicyConstants.LoginRole"/> role already
|
||||
/// </summary>
|
||||
/// <param name="unitOfWork"></param>
|
||||
/// <param name="userManager"></param>
|
||||
/// <param name="logger"></param>
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, UserManager<AppUser> userManager, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.6.0 introduced a change in how Normalization works and hence every normalized field needs to be re-calculated
|
||||
/// </summary>
|
||||
public static class MigrateNormalizedEverything
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.5.6 introduced Normalized Localized Name, which allows for faster lookups and less memory usage. This migration will calculate them once
|
||||
/// </summary>
|
||||
public static class MigrateNormalizedLocalizedName
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// New role introduced in v0.6. Calculates the Age Rating on all Reading Lists
|
||||
/// </summary>
|
||||
public static class MigrateReadingListAgeRating
|
||||
{
|
||||
/// <summary>
|
||||
/// Will not run if any above v0.5.6.24 or v0.6.0
|
||||
/// </summary>
|
||||
/// <param name="unitOfWork"></param>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="readingListService"></param>
|
||||
/// <param name="logger"></param>
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext context, IReadingListService readingListService, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Services.Tasks;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Added in v0.7.2.7/v0.7.3 in which the ConvertXToWebP Setting keys were removed. This migration will remove them.
|
||||
/// </summary>
|
||||
public static class MigrateRemoveWebPSettingRows
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, ILogger<Program> 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");
|
||||
}
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.38 or v0.7.0,
|
||||
/// </summary>
|
||||
public static class MigrateToUtcDates
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> 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");
|
||||
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.8 and v0.7, this adds library ids to all User Progress to allow for easier queries against progress
|
||||
/// </summary>
|
||||
public static class MigrateUserProgressLibraryId
|
||||
{
|
||||
public static async Task Migrate(IUnitOfWork unitOfWork, ILogger<Program> 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");
|
||||
}
|
||||
}
|
2504
API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs
generated
Normal file
2504
API/Data/Migrations/20231113215006_LibraryFileTypes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
API/Data/Migrations/20231113215006_LibraryFileTypes.cs
Normal file
46
API/Data/Migrations/20231113215006_LibraryFileTypes.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class LibraryFileTypes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryFileTypeGroup",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
LibraryId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
FileTypeGroup = table.Column<int>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryFileTypeGroup");
|
||||
}
|
||||
}
|
||||
}
|
2536
API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs
generated
Normal file
2536
API/Data/Migrations/20231117234829_LibraryExcludePatterns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs
Normal file
46
API/Data/Migrations/20231117234829_LibraryExcludePatterns.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class LibraryExcludePatterns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LibraryExcludePattern",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Pattern = table.Column<string>(type: "TEXT", nullable: true),
|
||||
LibraryId = table.Column<int>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LibraryExcludePattern");
|
||||
}
|
||||
}
|
||||
}
|
@ -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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LibraryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Pattern")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("LibraryExcludePattern");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.LibraryFileTypeGroup", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("FileTypeGroup")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("LibraryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LibraryId");
|
||||
|
||||
b.ToTable("LibraryFileTypeGroup");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.MangaFile", b =>
|
||||
{
|
||||
b.Property<int>("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");
|
||||
});
|
||||
|
||||
|
@ -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<LibraryDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
@ -100,12 +103,10 @@ public class LibraryRepository : ILibraryRepository
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<Library>> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -142,11 +143,10 @@ public class LibraryRepository : ILibraryRepository
|
||||
|
||||
public async Task<IEnumerable<Library>> GetLibraryForIdsAsync(IEnumerable<int> 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<int> 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<LibraryDto>(_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<Library> AddIncludesToQuery(IQueryable<Library> 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<bool> LibraryExists(string libraryName)
|
||||
{
|
||||
|
19
API/Entities/Enums/FileTypeGroup.cs
Normal file
19
API/Entities/Enums/FileTypeGroup.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace API.Entities.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a set of file types that can be scanned
|
||||
/// </summary>
|
||||
public enum FileTypeGroup
|
||||
{
|
||||
[Description("Archive")]
|
||||
Archive = 1,
|
||||
[Description("EPub")]
|
||||
Epub = 2,
|
||||
[Description("Pdf")]
|
||||
Pdf = 3,
|
||||
[Description("Images")]
|
||||
Images = 4
|
||||
|
||||
}
|
@ -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<FolderPath> Folders { get; set; } = null!;
|
||||
public ICollection<AppUser> AppUsers { get; set; } = null!;
|
||||
public ICollection<Series> Series { get; set; } = null!;
|
||||
public ICollection<LibraryFileTypeGroup> LibraryFileTypes { get; set; } = new List<LibraryFileTypeGroup>();
|
||||
public ICollection<LibraryExcludePattern> LibraryExcludePatterns { get; set; } = new List<LibraryExcludePattern>();
|
||||
|
||||
public void UpdateLastModified()
|
||||
{
|
||||
|
10
API/Entities/LibraryExcludedGlob.cs
Normal file
10
API/Entities/LibraryExcludedGlob.cs
Normal file
@ -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!;
|
||||
}
|
12
API/Entities/LibraryFileTypeGroup.cs
Normal file
12
API/Entities/LibraryFileTypeGroup.cs
Normal file
@ -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!;
|
||||
}
|
25
API/Extensions/FileTypeGroupExtensions.cs
Normal file
25
API/Extensions/FileTypeGroupExtensions.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -173,4 +173,34 @@ public static class IncludesExtensions
|
||||
|
||||
return queryable.AsSplitQuery();
|
||||
}
|
||||
|
||||
public static IQueryable<Library> Includes(this IQueryable<Library> 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();
|
||||
}
|
||||
}
|
||||
|
@ -208,7 +208,13 @@ public class AutoMapperProfiles : Profile
|
||||
CreateMap<Library, LibraryDto>()
|
||||
.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<AppUser, MemberDto>()
|
||||
.ForMember(dest => dest.AgeRestriction,
|
||||
|
@ -64,7 +64,7 @@ public interface IDirectoryService
|
||||
IEnumerable<string> GetDirectories(string folderPath);
|
||||
IEnumerable<string> GetDirectories(string folderPath, GlobMatcher? matcher);
|
||||
string GetParentDirectoryName(string fileOrFolder);
|
||||
IList<string> ScanFiles(string folderPath, GlobMatcher? matcher = null);
|
||||
IList<string> ScanFiles(string folderPath, string fileTypes, GlobMatcher? matcher = null);
|
||||
DateTime GetLastWriteTime(string folderPath);
|
||||
GlobMatcher? CreateMatcherFromFile(string filePath);
|
||||
}
|
||||
@ -646,7 +646,7 @@ public class DirectoryService : IDirectoryService
|
||||
/// <param name="folderPath"></param>
|
||||
/// <param name="matcher"></param>
|
||||
/// <returns></returns>
|
||||
public IList<string> ScanFiles(string folderPath, GlobMatcher? matcher = null)
|
||||
public IList<string> ScanFiles(string folderPath, string supportedExtensions, GlobMatcher? matcher = null)
|
||||
{
|
||||
_logger.LogDebug("[ScanFiles] called on {Path}", folderPath);
|
||||
var files = new List<string>();
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
/// <param name="folderAction">A callback async Task to be called once all files for each folder path are found</param>
|
||||
/// <param name="forceCheck">If we should bypass any folder last write time checks on the scan and force I/O</param>
|
||||
public async Task ProcessFiles(string folderPath, bool scanDirectoryByDirectory,
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<IList<string>, string,Task> folderAction, bool forceCheck = false)
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<IList<string>, 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -268,25 +285,24 @@ public class ParseScannedFiles
|
||||
/// <summary>
|
||||
/// This will process series by folder groups. This is used solely by ScanSeries
|
||||
/// </summary>
|
||||
/// <param name="libraryType"></param>
|
||||
/// <param name="library">This should have the FileTypes included</param>
|
||||
/// <param name="folders"></param>
|
||||
/// <param name="libraryName"></param>
|
||||
/// <param name="isLibraryScan">If true, does a directory scan first (resulting in folders being tackled in parallel), else does an immediate scan files</param>
|
||||
/// <param name="seriesPaths">A map of Series names -> existing folder paths to handle skipping folders</param>
|
||||
/// <param name="processSeriesInfos">Action which returns if the folder was skipped and the infos from said folder</param>
|
||||
/// <param name="forceCheck">Defaults to false</param>
|
||||
/// <returns></returns>
|
||||
public async Task ScanLibrariesForSeries(LibraryType libraryType,
|
||||
IEnumerable<string> folders, string libraryName, bool isLibraryScan,
|
||||
public async Task ScanLibrariesForSeries(Library library,
|
||||
IEnumerable<string> folders, bool isLibraryScan,
|
||||
IDictionary<string, IList<SeriesModified>> seriesPaths, Func<Tuple<bool, IList<ParserInfo>>, 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<string> files, string folder)
|
||||
@ -311,13 +327,13 @@ public class ParseScannedFiles
|
||||
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(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<ParsedSeries, List<ParserInfo>>();
|
||||
var infos = files
|
||||
.Select(file => _readingItemService.ParseFile(file, folder, libraryType))
|
||||
.Select(file => _readingItemService.ParseFile(file, folder, library.Type))
|
||||
.Where(info => info != null)
|
||||
.ToList();
|
||||
|
||||
|
@ -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 = @"._";
|
||||
|
||||
|
@ -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<Func<Task>>();
|
||||
//var processTasks = new List<Func<Task>>();
|
||||
|
||||
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;
|
||||
|
@ -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);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AgeRestriction } from '../metadata/age-restriction';
|
||||
import { Library } from '../library';
|
||||
import { Library } from '../library/library';
|
||||
|
||||
export interface Member {
|
||||
id: number;
|
||||
|
10
UI/Web/src/app/_models/library/file-type-group.enum.ts
Normal file
10
UI/Web/src/app/_models/library/file-type-group.enum.ts
Normal file
@ -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[];
|
@ -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<FileTypeGroup>;
|
||||
excludePatterns: Array<string>;
|
||||
}
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
@ -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<ReadingListItem>;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
count: number;
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
25
UI/Web/src/app/_pipes/file-type-group.pipe.ts
Normal file
25
UI/Web/src/app/_pipes/file-type-group.pipe.ts
Normal file
@ -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');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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({
|
@ -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';
|
||||
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
|
@ -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';
|
||||
|
@ -351,24 +351,7 @@
|
||||
<a ngbNavLink>{{t(tabs[TabID.WebLinks])}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<p>{{t('web-link-description')}}</p>
|
||||
<div class="row g-0 mb-3" *ngFor="let link of WebLinks; let i = index;">
|
||||
<div class="col-lg-8 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="web-link--{{i}}" class="visually-hidden">{{t('web-link-label')}}</label>
|
||||
<input type="text" class="form-control" formControlName="link{{i}}" attr.id="web-link--{{i}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary me-1" (click)="addWebLink()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('add-link-alt')}}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" (click)="removeWebLink(i)">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-link-alt')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-edit-list [items]="WebLinks" [label]="t('web-link-label')" (updateItems)="updateWeblinks($event)"></app-edit-list>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
@ -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<void> = 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<Person[]>);
|
||||
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<string>) {
|
||||
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();
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
|
@ -141,6 +141,7 @@
|
||||
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else storylineListLayout">
|
||||
<div class="card-container row g-0" #container>
|
||||
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
|
||||
{{item.id}}
|
||||
<ng-container [ngSwitch]="item.isChapter">
|
||||
<ng-container *ngSwitchCase="false" [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item.volume, scroll: scroll, idx: idx, volumesLength: volumes.length}"></ng-container>
|
||||
<ng-container *ngSwitchCase="true" [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container>
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
|
@ -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 ? ' ' : '');
|
||||
}
|
||||
|
24
UI/Web/src/app/shared/edit-list/edit-list.component.html
Normal file
24
UI/Web/src/app/shared/edit-list/edit-list.component.html
Normal file
@ -0,0 +1,24 @@
|
||||
<form [formGroup]="form" *transloco="let t">
|
||||
|
||||
@for(item of Items; let i = $index; track item) {
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-lg-10 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
<label for="item--{{i}}" class="visually-hidden">{{label}}</label>
|
||||
<input type="text" class="form-control" formControlName="link{{i}}" attr.id="item--{{i}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary me-1" (click)="add()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.add')}}</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" (click)="remove(i)">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.remove')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</form>
|
82
UI/Web/src/app/shared/edit-list/edit-list.component.ts
Normal file
82
UI/Web/src/app/shared/edit-list/edit-list.component.ts
Normal file
@ -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<string> = [];
|
||||
@Input({required: true}) label = '';
|
||||
@Output() updateItems = new EventEmitter<Array<string>>();
|
||||
|
||||
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 !== ''));
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +88,48 @@
|
||||
<li [ngbNavItem]="TabID.Advanced" [disabled]="isAddLibrary && setupStep < 3">
|
||||
<a ngbNavLink>{{t(TabID.Advanced)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<h6>{{t('file-type-group-label')}}</h6>
|
||||
<p class="accent">
|
||||
{{t('file-type-group-tooltip')}}
|
||||
</p>
|
||||
<div class="hstack gap-2">
|
||||
<div class="form-check form-switch" *ngFor="let group of fileTypeGroups; let i = index">
|
||||
<input class="form-check-input" [formControlName]="group" type="checkbox" [id]="group">
|
||||
<label class="form-check-label" [for]="group">{{ group | fileTypeGroup }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div ngbAccordion>
|
||||
<div ngbAccordionItem>
|
||||
<h2 ngbAccordionHeader>
|
||||
<button ngbAccordionButton>{{t('exclude-patterns-label')}}</button>
|
||||
</h2>
|
||||
<div ngbAccordionCollapse>
|
||||
<div ngbAccordionBody>
|
||||
<ng-template>
|
||||
<span class="mb-2">{{t('exclude-patterns-tooltip')}}</span>
|
||||
<a class="ms-1" href="https://wiki.kavitareader.com/en/guides/managing-your-files/scanner/excluding-files-folders" rel="noopener noreferrer" target="_blank">{{t('help')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
|
||||
<app-edit-list [items]="excludePatterns" [label]="t('exclude-patterns-label')" (updateItems)="updateGlobs($event)"></app-edit-list>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
|
@ -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<string> = [''];
|
||||
|
||||
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<string>) {
|
||||
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;
|
||||
|
@ -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<StatCount<Library>>;
|
||||
mostReadSeries: Array<StatCount<Series>>;
|
||||
recentlyRead: Array<Series>;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
111
openapi.json
111
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user