diff --git a/API.Tests/Parser/ParserTest.cs b/API.Tests/Parser/ParserTest.cs index cbb25f57b..e2f06465b 100644 --- a/API.Tests/Parser/ParserTest.cs +++ b/API.Tests/Parser/ParserTest.cs @@ -68,6 +68,7 @@ public class ParserTests [InlineData("(C99) Kami-sama Hiroimashita. (SSSS.GRIDMAN)", false, "Kami-sama Hiroimashita.")] [InlineData("Dr. Ramune - Mysterious Disease Specialist v01 (2020) (Digital) (danke-Empire)", false, "Dr. Ramune - Mysterious Disease Specialist v01")] [InlineData("Magic Knight Rayearth {Omnibus Edition}", false, "Magic Knight Rayearth {}")] + [InlineData("Magic Knight Rayearth {Omnibus Version}", false, "Magic Knight Rayearth { Version}")] public void CleanTitleTest(string input, bool isComic, string expected) { Assert.Equal(expected, CleanTitle(input, isComic)); diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index b62acb1d1..71ecc1543 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -471,6 +471,53 @@ public class ReaderServiceTests Assert.Equal("21", actualChapter.Range); } + [Fact] + public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolumeWithFloat() + { + await ResetDb(); + + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("1", new List() + { + EntityFactory.CreateChapter("1", false, new List()), + EntityFactory.CreateChapter("2", false, new List()), + }), + EntityFactory.CreateVolume("1.5", new List() + { + EntityFactory.CreateChapter("21", false, new List()), + EntityFactory.CreateChapter("22", false, new List()), + }), + EntityFactory.CreateVolume("2", new List() + { + EntityFactory.CreateChapter("31", false, new List()), + EntityFactory.CreateChapter("32", false, new List()), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); + + + var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); + Assert.Equal("21", actualChapter.Range); + } + [Fact] public async Task GetNextChapterIdAsync_ShouldRollIntoChaptersFromVolume() { @@ -895,6 +942,53 @@ public class ReaderServiceTests Assert.Equal("1", actualChapter.Range); } + [Fact] + public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume_WithFloatVolume() + { + // V1 -> V2 + await ResetDb(); + + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("1", new List() + { + EntityFactory.CreateChapter("1", false, new List()), + EntityFactory.CreateChapter("2", false, new List()), + }), + EntityFactory.CreateVolume("1.5", new List() + { + EntityFactory.CreateChapter("21", false, new List()), + EntityFactory.CreateChapter("22", false, new List()), + }), + EntityFactory.CreateVolume("3", new List() + { + EntityFactory.CreateChapter("31", false, new List()), + EntityFactory.CreateChapter("32", false, new List()), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), Substitute.For()); + + var prevChapter = await readerService.GetPrevChapterIdAsync(1, 3, 5, 1); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); + Assert.Equal("22", actualChapter.Range); + } + [Fact] public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume_2() { diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 98377c5fd..f62326651 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -112,10 +112,12 @@ public class LibraryController : BaseApiController return Ok(_directoryService.ListDirectory(path)); } + + [ResponseCache(CacheProfileName = "10Minute")] [HttpGet] public async Task>> GetLibraries() { - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync()); + return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername())); } [HttpGet("jump-bar")] @@ -196,12 +198,6 @@ public class LibraryController : BaseApiController return Ok(); } - [HttpGet("libraries")] - public async Task>> GetLibrariesForUser() - { - return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername())); - } - /// /// Given a valid path, will invoke either a Scan Series or Scan Library. If the folder does not exist within Kavita, the request will be ignored /// diff --git a/API/Controllers/MetadataController.cs b/API/Controllers/MetadataController.cs index 9c659637f..34525a9fc 100644 --- a/API/Controllers/MetadataController.cs +++ b/API/Controllers/MetadataController.cs @@ -78,7 +78,7 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all ratings /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(Duration = 60 * 5, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"libraryIds"})] + [ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"libraryIds"})] [HttpGet("age-ratings")] public async Task>> GetAllAgeRatings(string? libraryIds) { @@ -101,7 +101,7 @@ public class MetadataController : BaseApiController /// String separated libraryIds or null for all publication status /// This API is cached for 1 hour, varying by libraryIds /// - [ResponseCache(Duration = 60 * 5, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"libraryIds"})] + [ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"libraryIds"})] [HttpGet("publication-status")] public ActionResult> GetAllPublicationStatus(string? libraryIds) { diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 2ac1d3bf3..99b95b460 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -714,6 +714,7 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})] [HttpGet("next-chapter")] public async Task> GetNextChapter(int seriesId, int volumeId, int currentChapterId) { @@ -732,6 +733,7 @@ public class ReaderController : BaseApiController /// /// /// chapter id for next manga + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})] [HttpGet("prev-chapter")] public async Task> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId) { diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index 2304170e3..e6c97ea1c 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -194,6 +194,7 @@ public class SeriesController : BaseApiController return BadRequest("There was an error with updating the series"); } + [ResponseCache(CacheProfileName = "Instant")] [HttpPost("recently-added")] public async Task>> GetRecentlyAdded(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { @@ -211,6 +212,7 @@ public class SeriesController : BaseApiController return Ok(series); } + [ResponseCache(CacheProfileName = "Instant")] [HttpPost("recently-updated-series")] public async Task>> GetRecentlyAddedChapters() { @@ -242,6 +244,7 @@ public class SeriesController : BaseApiController /// /// Default of 0 meaning all libraries /// + [ResponseCache(CacheProfileName = "Instant")] [HttpPost("on-deck")] public async Task>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0) { @@ -364,7 +367,7 @@ public class SeriesController : BaseApiController /// /// /// This is cached for an hour - [ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"ageRating"})] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] {"ageRating"})] [HttpGet("age-rating")] public ActionResult GetAgeRating(int ageRating) { @@ -380,7 +383,7 @@ public class SeriesController : BaseApiController /// /// /// Do not rely on this API externally. May change without hesitation. - [ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"seriesId"})] + [ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] {"seriesId"})] [HttpGet("series-detail")] public async Task> GetSeriesDetailBreakdown(int seriesId) { diff --git a/API/Controllers/ThemeController.cs b/API/Controllers/ThemeController.cs index 6defbe574..d6a9b526e 100644 --- a/API/Controllers/ThemeController.cs +++ b/API/Controllers/ThemeController.cs @@ -24,6 +24,7 @@ public class ThemeController : BaseApiController _taskScheduler = taskScheduler; } + [ResponseCache(CacheProfileName = "10Minute")] [AllowAnonymous] [HttpGet] public async Task>> GetThemes() diff --git a/API/Data/MigrateReadingListAgeRating.cs b/API/Data/MigrateReadingListAgeRating.cs index ebaa1ce03..cc1ddfc3d 100644 --- a/API/Data/MigrateReadingListAgeRating.cs +++ b/API/Data/MigrateReadingListAgeRating.cs @@ -16,13 +16,14 @@ public static class MigrateReadingListAgeRating /// /// Will not run if any above v0.5.6.24 or v0.6.0 /// + /// /// /// /// public static async Task Migrate(IUnitOfWork unitOfWork, DataContext context, IReadingListService readingListService, ILogger logger) { var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); - if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 24)) + if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 26)) { return; } diff --git a/API/Data/Repositories/LibraryRepository.cs b/API/Data/Repositories/LibraryRepository.cs index ce31660c3..58c0faa11 100644 --- a/API/Data/Repositories/LibraryRepository.cs +++ b/API/Data/Repositories/LibraryRepository.cs @@ -188,6 +188,10 @@ public class LibraryRepository : ILibraryRepository }); } + /// + /// Returns all Libraries with their Folders + /// + /// public async Task> GetLibraryDtosAsync() { return await _context.Library diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 6edb4caf7..73ed20cca 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -1048,7 +1048,11 @@ public class SeriesRepository : ISeriesRepository var userRating = await GetUserAgeRestriction(userId); var items = (await GetRecentlyAddedChaptersQuery(userId)); - foreach (var item in items.Where(c => c.AgeRating <= userRating)) + if (userRating != AgeRating.NotApplicable) + { + items = items.Where(c => c.AgeRating <= userRating); + } + foreach (var item in items) { if (seriesMap.Keys.Count == pageSize) break; @@ -1215,7 +1219,8 @@ public class SeriesRepository : ISeriesRepository .Where(s => s.LibraryId == libraryId) .Where(s => s.Format == format && format != MangaFormat.Unknown) .Where(s => s.NormalizedName.Equals(normalizedSeries) - || (s.NormalizedLocalizedName.Equals(normalizedSeries) && s.NormalizedLocalizedName != string.Empty)); + || (s.NormalizedLocalizedName.Equals(normalizedSeries) && s.NormalizedLocalizedName != string.Empty) + || s.OriginalName.Equals(seriesName)); if (!string.IsNullOrEmpty(normalizedLocalized)) { diff --git a/API/Extensions/VolumeListExtensions.cs b/API/Extensions/VolumeListExtensions.cs index 7f4cc08a6..5c9084764 100644 --- a/API/Extensions/VolumeListExtensions.cs +++ b/API/Extensions/VolumeListExtensions.cs @@ -17,15 +17,17 @@ public static class VolumeListExtensions /// public static Volume GetCoverImage(this IList volumes, MangaFormat seriesFormat) { - if (seriesFormat is MangaFormat.Epub or MangaFormat.Pdf) + if (seriesFormat == MangaFormat.Epub || seriesFormat == MangaFormat.Pdf) { - return volumes.OrderBy(x => x.Number).FirstOrDefault(); + return volumes.MinBy(x => x.Number); } if (volumes.Any(x => x.Number != 0)) { return volumes.OrderBy(x => x.Number).FirstOrDefault(x => x.Number != 0); } - return volumes.OrderBy(x => x.Number).FirstOrDefault(); + + // We only have 1 volume of chapters, we need to be cautious if there are specials, as we don't want to order them first + return volumes.MinBy(x => x.Number); } } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index ef99aaacc..dbf7214cb 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -676,7 +676,7 @@ public class DirectoryService : IDirectoryService } GlobMatcher matcher = new(); - foreach (var line in lines) + foreach (var line in lines.Where(s => !string.IsNullOrEmpty(s))) { matcher.AddExclude(line); } diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 8eed9ebe9..767655698 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -51,7 +51,6 @@ public class MetadataService : IMetadataService private readonly ICacheHelper _cacheHelper; private readonly IReadingItemService _readingItemService; private readonly IDirectoryService _directoryService; - private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst(); private readonly IList _updateEvents = new List(); public MetadataService(IUnitOfWork unitOfWork, ILogger logger, IEventHub eventHub, ICacheHelper cacheHelper, @@ -108,7 +107,7 @@ public class MetadataService : IMetadataService volume.Chapters ??= new List(); - var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting); + var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), ChapterSortComparerZeroFirst.Default); if (firstChapter == null) return Task.FromResult(false); volume.CoverImage = firstChapter.CoverImage; @@ -133,20 +132,14 @@ public class MetadataService : IMetadataService series.Volumes ??= new List(); var firstCover = series.Volumes.GetCoverImage(series.Format); string coverImage = null; - if (firstCover == null && series.Volumes.Any()) - { - // If firstCover is null and one volume, the whole series is Chapters under Vol 0. - if (series.Volumes.Count == 1) - { - coverImage = series.Volumes[0].Chapters.OrderBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting) - .FirstOrDefault(c => !c.IsSpecial)?.CoverImage; - } - if (!_cacheHelper.CoverImageExists(coverImage)) - { - coverImage = series.Volumes[0].Chapters.MinBy(c => double.Parse(c.Number), _chapterSortComparerForInChapterSorting)?.CoverImage; - } + var chapters = firstCover.Chapters.OrderBy(c => double.Parse(c.Number), ChapterSortComparerZeroFirst.Default).ToList(); + if (chapters.Count > 1 && chapters.Any(c => c.IsSpecial)) + { + coverImage = chapters.First(c => !c.IsSpecial).CoverImage ?? chapters.First().CoverImage; + firstCover = null; } + series.CoverImage = firstCover?.CoverImage ?? coverImage; _updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series)); return Task.CompletedTask; diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 86f027566..40fe149fa 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -313,10 +313,12 @@ public class ReaderService : IReaderService if (chapterId > 0) return chapterId; } + var currentVolumeNumber = float.Parse(currentVolume.Name); var next = false; foreach (var volume in volumes) { - if (volume.Number == currentVolume.Number && volume.Chapters.Count > 1) + var volumeNumbersMatch = Math.Abs(float.Parse(volume.Name) - currentVolumeNumber) < 0.00001f; + if (volumeNumbersMatch && volume.Chapters.Count > 1) { // Handle Chapters within current Volume // In this case, i need 0 first because 0 represents a full volume file. @@ -327,7 +329,7 @@ public class ReaderService : IReaderService continue; } - if (volume.Number == currentVolume.Number) + if (volumeNumbersMatch) { next = true; continue; diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 070bdb5e7..f5efa18cf 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -224,17 +224,14 @@ public class TaskScheduler : ITaskScheduler public void ScanLibrary(int libraryId, bool force = false) { - var alreadyEnqueued = - HasAlreadyEnqueuedTask("ScannerService", "ScanLibrary", new object[] {libraryId, true}, ScanQueue) || - HasAlreadyEnqueuedTask("ScannerService", "ScanLibrary", new object[] {libraryId, false}, ScanQueue); - if (alreadyEnqueued) + if (HasScanTaskRunningForLibrary(libraryId)) { - _logger.LogInformation("A duplicate request to scan library for library occured. Skipping"); + _logger.LogInformation("A duplicate request for Library Scan on library {LibraryId} occured. Skipping", libraryId); return; } if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) { - _logger.LogInformation("A Scan is already running, rescheduling ScanLibrary in 3 hours"); + _logger.LogInformation("A Library Scan is already running, rescheduling ScanLibrary in 3 hours"); BackgroundJob.Schedule(() => ScanLibrary(libraryId, force), TimeSpan.FromHours(3)); return; } @@ -324,27 +321,29 @@ public class TaskScheduler : ITaskScheduler } /// - /// If there is an enqueued or scheduled tak for method + /// If there is an enqueued or scheduled task for method /// /// + /// Checks against jobs currently executing as well /// - public static bool HasScanTaskRunningForLibrary(int libraryId) + public static bool HasScanTaskRunningForLibrary(int libraryId, bool checkRunningJobs = true) { return - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", new object[] {libraryId, true}, ScanQueue) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", new object[] {libraryId, false}, ScanQueue); + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", new object[] {libraryId, true}, ScanQueue, checkRunningJobs) || + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanLibrary", new object[] {libraryId, false}, ScanQueue, checkRunningJobs); } /// - /// If there is an enqueued or scheduled tak for method + /// If there is an enqueued or scheduled task for method /// /// + /// Checks against jobs currently executing as well /// - public static bool HasScanTaskRunningForSeries(int seriesId) + public static bool HasScanTaskRunningForSeries(int seriesId, bool checkRunningJobs = true) { return - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, true}, ScanQueue) || - HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, false}, ScanQueue); + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, true}, ScanQueue, checkRunningJobs) || + HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", new object[] {seriesId, false}, ScanQueue, checkRunningJobs); } /// @@ -354,8 +353,9 @@ public class TaskScheduler : ITaskScheduler /// Class name the method resides on /// object[] of arguments in the order they are passed to enqueued job /// Queue to check against. Defaults to "default" + /// Check against running jobs. Defaults to false. /// - public static bool HasAlreadyEnqueuedTask(string className, string methodName, object[] args, string queue = DefaultQueue) + public static bool HasAlreadyEnqueuedTask(string className, string methodName, object[] args, string queue = DefaultQueue, bool checkRunningJobs = false) { var enqueuedJobs = JobStorage.Current.GetMonitoringApi().EnqueuedJobs(queue, 0, int.MaxValue); var ret = enqueuedJobs.Any(j => j.Value.InEnqueuedState && @@ -365,10 +365,23 @@ public class TaskScheduler : ITaskScheduler if (ret) return true; var scheduledJobs = JobStorage.Current.GetMonitoringApi().ScheduledJobs(0, int.MaxValue); - return scheduledJobs.Any(j => + ret = scheduledJobs.Any(j => j.Value.Job.Method.DeclaringType != null && j.Value.Job.Args.SequenceEqual(args) && j.Value.Job.Method.Name.Equals(methodName) && j.Value.Job.Method.DeclaringType.Name.Equals(className)); + + if (ret) return true; + + if (checkRunningJobs) + { + var runningJobs = JobStorage.Current.GetMonitoringApi().ProcessingJobs(0, int.MaxValue); + return runningJobs.Any(j => + j.Value.Job.Method.DeclaringType != null && j.Value.Job.Args.SequenceEqual(args) && + j.Value.Job.Method.Name.Equals(methodName) && + j.Value.Job.Method.DeclaringType.Name.Equals(className)); + } + + return false; } /// diff --git a/API/Services/Tasks/Scanner/LibraryWatcher.cs b/API/Services/Tasks/Scanner/LibraryWatcher.cs index 1143b0622..fea30b7fe 100644 --- a/API/Services/Tasks/Scanner/LibraryWatcher.cs +++ b/API/Services/Tasks/Scanner/LibraryWatcher.cs @@ -248,7 +248,7 @@ public class LibraryWatcher : ILibraryWatcher if (!rootFolder.Any()) return string.Empty; // Select the first folder and join with library folder, this should give us the folder to scan. - return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder.First())); + return Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(libraryFolder, rootFolder.Last())); } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index d36fb3cff..44be4c02b 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -112,16 +112,29 @@ public class ParseScannedFiles return; } // 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); + } + + /// + /// Used in ScanSeries, which enters at a lower level folder and hence needs a .kavitaignore from higher (up to root) to be built before + /// the scan takes place. + /// + /// + /// + /// A GlobMatter. Empty if not applicable + private GlobMatcher BuildIgnoreFromLibraryRoot(string folderPath, IDictionary> seriesPaths) + { var seriesMatcher = new GlobMatcher(); try { - var roots = seriesPaths[folderPath][0].LibraryRoots.Select(Scanner.Parser.Parser.NormalizePath).ToList(); + var roots = seriesPaths[folderPath][0].LibraryRoots.Select(Parser.Parser.NormalizePath).ToList(); var libraryFolder = roots.SingleOrDefault(folderPath.Contains); if (string.IsNullOrEmpty(libraryFolder) || !Directory.Exists(folderPath)) { - await folderAction(_directoryService.ScanFiles(folderPath, seriesMatcher), folderPath); - return; + return seriesMatcher; } var allParents = _directoryService.GetFoldersTillRoot(libraryFolder, folderPath); @@ -141,11 +154,11 @@ public class ParseScannedFiles } catch (Exception ex) { - _logger.LogError(ex, "There was an error trying to find and apply .kavitaignores above the Series Folder. Scanning without them present"); + _logger.LogError(ex, + "There was an error trying to find and apply .kavitaignores above the Series Folder. Scanning without them present"); } - - await folderAction(_directoryService.ScanFiles(folderPath, seriesMatcher), folderPath); + return seriesMatcher; } @@ -248,7 +261,7 @@ public class ParseScannedFiles /// - /// This will process series by folder groups. + /// This will process series by folder groups. This is used solely by ScanSeries /// /// /// @@ -285,7 +298,7 @@ public class ParseScannedFiles MessageFactory.FileScanProgressEvent(folder, libraryName, ProgressEventType.Updated)); if (files.Count == 0) { - _logger.LogInformation("[ScannerService] {Folder} is empty", folder); + _logger.LogInformation("[ScannerService] {Folder} is empty or is no longer in this location", folder); return; } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index bc28ff578..f4c64c96b 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -192,6 +192,7 @@ 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)) @@ -219,7 +220,8 @@ public class ScannerService : IScannerService Format = parsedFiles.First().Format }; - if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName)) + // For Scan Series, we need to filter out anything that isn't our Series + if (!foundParsedSeries.NormalizedName.Equals(series.NormalizedName) && !foundParsedSeries.NormalizedName.Equals(Scanner.Parser.Parser.Normalize(series.OriginalName))) { return; } @@ -506,7 +508,9 @@ public class ScannerService : IScannerService // Could I delete anything in a Library's Series where the LastScan date is before scanStart? // NOTE: This implementation is expensive + _logger.LogDebug("Removing Series that were not found during the scan"); var removedSeries = await _unitOfWork.SeriesRepository.RemoveSeriesNotInList(seenSeries, library.Id); + _logger.LogDebug("Removing Series that were not found during the scan - complete"); _unitOfWork.LibraryRepository.Update(library); if (await _unitOfWork.CommitAsync()) diff --git a/API/Startup.cs b/API/Startup.cs index ed7181816..a2fe63153 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -66,10 +66,30 @@ public class Startup options.CacheProfiles.Add("Hour", new CacheProfile() { - Duration = 60 * 10, + Duration = 60 * 60, Location = ResponseCacheLocation.None, NoStore = false }); + options.CacheProfiles.Add("10Minute", + new CacheProfile() + { + Duration = 60 * 10, + Location = ResponseCacheLocation.Any, + NoStore = false + }); + options.CacheProfiles.Add("5Minute", + new CacheProfile() + { + Duration = 60 * 5, + Location = ResponseCacheLocation.Any, + }); + // Instant is a very quick cache, because we can't bust based on the query params, but rather body + options.CacheProfiles.Add("Instant", + new CacheProfile() + { + Duration = 30, + Location = ResponseCacheLocation.Any, + }); }); services.Configure(options => { diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index ffbd54c84..cd72f2fce 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -243,7 +243,7 @@ export class ActionFactoryService { action: Action.Scan, title: 'Scan Series', callback: this.dummyCallback, - requiresAdmin: false, + requiresAdmin: true, children: [], }, { @@ -304,7 +304,7 @@ export class ActionFactoryService { action: Action.Submenu, title: 'Others', callback: this.dummyCallback, - requiresAdmin: false, + requiresAdmin: true, children: [ { action: Action.RefreshMetadata, diff --git a/UI/Web/src/app/_services/library.service.ts b/UI/Web/src/app/_services/library.service.ts index 00619d5bd..403fd409e 100644 --- a/UI/Web/src/app/_services/library.service.ts +++ b/UI/Web/src/app/_services/library.service.ts @@ -1,11 +1,10 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { of } from 'rxjs'; -import { map, take } from 'rxjs/operators'; +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 { SearchResultGroup } from '../_models/search/search-result-group'; import { DirectoryDto } from '../_models/system/directory-dto'; @@ -68,10 +67,6 @@ export class LibraryService { return this.httpClient.get(this.baseUrl + 'library'); } - getLibrariesForMember() { - return this.httpClient.get(this.baseUrl + 'library/libraries'); - } - updateLibrariesForMember(username: string, selectedLibraries: Library[]) { return this.httpClient.post(this.baseUrl + 'library/grant-access', {username, selectedLibraries}); } diff --git a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts index 212af28fa..9ac9d1b44 100644 --- a/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-series-modal/edit-series-modal.component.ts @@ -137,7 +137,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy { ageRating: new FormControl('', []), publicationStatus: new FormControl('', []), language: new FormControl('', []), - releaseYear: new FormControl('', [Validators.minLength(4), Validators.maxLength(4), Validators.pattern(/[1-9]\d{3}/)]), + releaseYear: new FormControl('', [Validators.minLength(4), Validators.maxLength(4), Validators.pattern(/([1-9]\d{3})|[0]{1}/)]), }); this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html index 33c2be6c1..548d2b7e8 100644 --- a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html @@ -1,6 +1,7 @@
- +
@@ -8,7 +9,7 @@ - + @@ -24,7 +25,7 @@
- +
diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts index cba92e33e..91a637eae 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts @@ -72,10 +72,6 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy { coverImageUrl: new FormControl('', []) }); - this.imageUrls.forEach(url => { - - }); - console.log('imageUrls: ', this.imageUrls); this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/dashboard/dashboard.component.ts b/UI/Web/src/app/dashboard/dashboard.component.ts index 86a2d91b0..0f7fc167f 100644 --- a/UI/Web/src/app/dashboard/dashboard.component.ts +++ b/UI/Web/src/app/dashboard/dashboard.component.ts @@ -90,7 +90,7 @@ export class DashboardComponent implements OnInit, OnDestroy { this.isLoading = true; this.cdRef.markForCheck(); - this.libraries$ = this.libraryService.getLibrariesForMember().pipe(take(1), tap((libs) => { + this.libraries$ = this.libraryService.getLibraries().pipe(take(1), tap((libs) => { this.isLoading = false; this.cdRef.markForCheck(); })); diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts index a6b375076..69286b85f 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.ts +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.ts @@ -270,7 +270,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy { this.librarySettings.unique = true; this.librarySettings.addIfNonExisting = false; this.librarySettings.fetchFn = (filter: string) => { - return this.libraryService.getLibrariesForMember() + return this.libraryService.getLibraries() .pipe(map(items => this.librarySettings.compareFn(items, filter))); }; this.librarySettings.compareFn = (options: Library[], filter: string) => { diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts index a2e10c957..5b7ef30d8 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts @@ -54,7 +54,7 @@ export class SideNavComponent implements OnInit, OnDestroy { ngOnInit(): void { this.accountService.currentUser$.pipe(take(1)).subscribe(user => { if (user) { - this.libraryService.getLibrariesForMember().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { + this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { this.libraries = libraries; this.cdRef.markForCheck(); }); @@ -64,7 +64,7 @@ export class SideNavComponent implements OnInit, OnDestroy { }); this.messageHub.messages$.pipe(takeUntil(this.onDestroy), filter(event => event.event === EVENTS.LibraryModified)).subscribe(event => { - this.libraryService.getLibrariesForMember().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { + this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { this.libraries = libraries; this.cdRef.markForCheck(); }); diff --git a/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.ts b/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.ts index d6e075391..5c4958775 100644 --- a/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.ts +++ b/UI/Web/src/app/user-settings/change-age-restriction/change-age-restriction.component.ts @@ -50,7 +50,6 @@ export class ChangeAgeRestrictionComponent implements OnInit { resetForm() { if (!this.user) return; - console.log('resetting to ', this.originalRating) this.reset.emit(this.originalRating); this.cdRef.markForCheck(); }