Misc Bugfixes (#1582)

* Fixed a bug with RBS on non-admin accounts

* Fixed a bug where get next/prev chapter wouldn't respect floating point volume numbers

* Fixed a bad migration version check

* When building kavita ignore exclusions, ignore blank lines.

* Hooked up the GetFullSeriesByAnyName to check against OriginalName exactly

* Refactored some code for building ignore from library root, to keep the code cleaner

* Tweaked some messaging

* Fixed a bad directory join when a change event occurs in a nested series folder.

* Fixed a bug where cover generation would prioritize a special if there were only chapters in the series.

* Fixed a bug where you couldn't update a series modal if there wasn't a release year present

* Fixed an issue where renaming the Series in Kavita wouldn't allow ScanSeries to see the files, and thus would delete the Series.

* Added an additional check with Hangfire to make sure ScanFolder doesn't kick off a change when a bunch of changes come through for the same directory, but a job is already running.

* Added more documentation

* Migrated more response caching to profiles and merged 2 apis into one, since they do the same thing.

* Fixed a bug where NotApplicable age ratings were breaking Recently Updated Series

* Cleaned up some cache profiles

* More caching

* Provide response caching on Get Next/Prev Chapter

* Code smells
This commit is contained in:
Joe Milazzo 2022-10-10 19:23:37 -05:00 committed by GitHub
parent b6f6b0ed99
commit c652c36081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 227 additions and 82 deletions

View File

@ -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));

View File

@ -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<Volume>()
{
EntityFactory.CreateVolume("1", new List<Chapter>()
{
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
}),
EntityFactory.CreateVolume("1.5", new List<Chapter>()
{
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
}),
EntityFactory.CreateVolume("2", new List<Chapter>()
{
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
}),
}
});
_context.AppUser.Add(new AppUser()
{
UserName = "majora2007"
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
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<Volume>()
{
EntityFactory.CreateVolume("1", new List<Chapter>()
{
EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
}),
EntityFactory.CreateVolume("1.5", new List<Chapter>()
{
EntityFactory.CreateChapter("21", false, new List<MangaFile>()),
EntityFactory.CreateChapter("22", false, new List<MangaFile>()),
}),
EntityFactory.CreateVolume("3", new List<Chapter>()
{
EntityFactory.CreateChapter("31", false, new List<MangaFile>()),
EntityFactory.CreateChapter("32", false, new List<MangaFile>()),
}),
}
});
_context.AppUser.Add(new AppUser()
{
UserName = "majora2007"
});
await _context.SaveChangesAsync();
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
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()
{

View File

@ -112,10 +112,12 @@ public class LibraryController : BaseApiController
return Ok(_directoryService.ListDirectory(path));
}
[ResponseCache(CacheProfileName = "10Minute")]
[HttpGet]
public async Task<ActionResult<IEnumerable<LibraryDto>>> 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<ActionResult<IEnumerable<LibraryDto>>> GetLibrariesForUser()
{
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
}
/// <summary>
/// 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
/// </summary>

View File

@ -78,7 +78,7 @@ public class MetadataController : BaseApiController
/// <param name="libraryIds">String separated libraryIds or null for all ratings</param>
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
/// <returns></returns>
[ResponseCache(Duration = 60 * 5, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"libraryIds"})]
[ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"libraryIds"})]
[HttpGet("age-ratings")]
public async Task<ActionResult<IList<AgeRatingDto>>> GetAllAgeRatings(string? libraryIds)
{
@ -101,7 +101,7 @@ public class MetadataController : BaseApiController
/// <param name="libraryIds">String separated libraryIds or null for all publication status</param>
/// <remarks>This API is cached for 1 hour, varying by libraryIds</remarks>
/// <returns></returns>
[ResponseCache(Duration = 60 * 5, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"libraryIds"})]
[ResponseCache(CacheProfileName = "5Minute", VaryByQueryKeys = new [] {"libraryIds"})]
[HttpGet("publication-status")]
public ActionResult<IList<AgeRatingDto>> GetAllPublicationStatus(string? libraryIds)
{

View File

@ -714,6 +714,7 @@ public class ReaderController : BaseApiController
/// <param name="volumeId"></param>
/// <param name="currentChapterId"></param>
/// <returns>chapter id for next manga</returns>
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})]
[HttpGet("next-chapter")]
public async Task<ActionResult<int>> GetNextChapter(int seriesId, int volumeId, int currentChapterId)
{
@ -732,6 +733,7 @@ public class ReaderController : BaseApiController
/// <param name="volumeId"></param>
/// <param name="currentChapterId"></param>
/// <returns>chapter id for next manga</returns>
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new string[] { "seriesId", "volumeId", "currentChapterId"})]
[HttpGet("prev-chapter")]
public async Task<ActionResult<int>> GetPreviousChapter(int seriesId, int volumeId, int currentChapterId)
{

View File

@ -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<ActionResult<IEnumerable<SeriesDto>>> 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<ActionResult<IEnumerable<RecentlyAddedItemDto>>> GetRecentlyAddedChapters()
{
@ -242,6 +244,7 @@ public class SeriesController : BaseApiController
/// <param name="userParams"></param>
/// <param name="libraryId">Default of 0 meaning all libraries</param>
/// <returns></returns>
[ResponseCache(CacheProfileName = "Instant")]
[HttpPost("on-deck")]
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
{
@ -364,7 +367,7 @@ public class SeriesController : BaseApiController
/// <param name="ageRating"></param>
/// <returns></returns>
/// <remarks>This is cached for an hour</remarks>
[ResponseCache(Duration = 60 * 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"ageRating"})]
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] {"ageRating"})]
[HttpGet("age-rating")]
public ActionResult<string> GetAgeRating(int ageRating)
{
@ -380,7 +383,7 @@ public class SeriesController : BaseApiController
/// <param name="seriesId"></param>
/// <returns></returns>
/// <remarks>Do not rely on this API externally. May change without hesitation. </remarks>
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new [] {"seriesId"})]
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] {"seriesId"})]
[HttpGet("series-detail")]
public async Task<ActionResult<SeriesDetailDto>> GetSeriesDetailBreakdown(int seriesId)
{

View File

@ -24,6 +24,7 @@ public class ThemeController : BaseApiController
_taskScheduler = taskScheduler;
}
[ResponseCache(CacheProfileName = "10Minute")]
[AllowAnonymous]
[HttpGet]
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()

View File

@ -16,13 +16,14 @@ 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, 24))
if (Version.Parse(settings.InstallVersion) > new Version(0, 5, 6, 26))
{
return;
}

View File

@ -188,6 +188,10 @@ public class LibraryRepository : ILibraryRepository
});
}
/// <summary>
/// Returns all Libraries with their Folders
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
{
return await _context.Library

View File

@ -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))
{

View File

@ -17,15 +17,17 @@ public static class VolumeListExtensions
/// <returns></returns>
public static Volume GetCoverImage(this IList<Volume> 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);
}
}

View File

@ -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);
}

View File

@ -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<SignalRMessage> _updateEvents = new List<SignalRMessage>();
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
IEventHub eventHub, ICacheHelper cacheHelper,
@ -108,7 +107,7 @@ public class MetadataService : IMetadataService
volume.Chapters ??= new List<Chapter>();
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<Volume>();
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;

View File

@ -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;

View File

@ -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
}
/// <summary>
/// If there is an enqueued or scheduled tak for <see cref="ScannerService.ScanLibrary"/> method
/// If there is an enqueued or scheduled task for <see cref="ScannerService.ScanLibrary"/> method
/// </summary>
/// <param name="libraryId"></param>
/// <param name="checkRunningJobs">Checks against jobs currently executing as well</param>
/// <returns></returns>
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);
}
/// <summary>
/// If there is an enqueued or scheduled tak for <see cref="ScannerService.ScanSeries"/> method
/// If there is an enqueued or scheduled task for <see cref="ScannerService.ScanSeries"/> method
/// </summary>
/// <param name="seriesId"></param>
/// <param name="checkRunningJobs">Checks against jobs currently executing as well</param>
/// <returns></returns>
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);
}
/// <summary>
@ -354,8 +353,9 @@ public class TaskScheduler : ITaskScheduler
/// <param name="className">Class name the method resides on</param>
/// <param name="args">object[] of arguments in the order they are passed to enqueued job</param>
/// <param name="queue">Queue to check against. Defaults to "default"</param>
/// <param name="checkRunningJobs">Check against running jobs. Defaults to false.</param>
/// <returns></returns>
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;
}
/// <summary>

View File

@ -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()));
}

View File

@ -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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="folderPath"></param>
/// <param name="seriesPaths"></param>
/// <returns>A GlobMatter. Empty if not applicable</returns>
private GlobMatcher BuildIgnoreFromLibraryRoot(string folderPath, IDictionary<string, IList<SeriesModified>> 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
/// <summary>
/// This will process series by folder groups.
/// This will process series by folder groups. This is used solely by ScanSeries
/// </summary>
/// <param name="libraryType"></param>
/// <param name="folders"></param>
@ -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;
}

View File

@ -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())

View File

@ -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<ForwardedHeadersOptions>(options =>
{

View File

@ -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,

View File

@ -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<Library[]>(this.baseUrl + 'library');
}
getLibrariesForMember() {
return this.httpClient.get<Library[]>(this.baseUrl + 'library/libraries');
}
updateLibrariesForMember(username: string, selectedLibraries: Library[]) {
return this.httpClient.post(this.baseUrl + 'library/grant-access', {username, selectedLibraries});
}

View File

@ -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();

View File

@ -1,6 +1,7 @@
<ng-container *ngIf="actions.length > 0">
<div ngbDropdown container="body" class="d-inline-block">
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle (click)="preventEvent($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle
(click)="preventEvent($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
</div>
@ -8,7 +9,7 @@
<ng-template #submenu let-list="list">
<ng-container *ngFor="let action of list">
<!-- Non Submenu items -->
<ng-container *ngIf="action.children === undefined || action?.children?.length === 0 || action.dynamicList != undefined; else submenuDropdown">
<ng-container *ngIf="action.children === undefined || action?.children?.length === 0 || action.dynamicList != undefined ; else submenuDropdown">
<ng-container *ngIf="action.dynamicList != undefined && (action.dynamicList | async | dynamicList) as dList; else justItem">
<ng-container *ngFor="let dynamicItem of dList">
@ -24,7 +25,7 @@
<!-- Submenu items -->
<ng-container *ngIf="shouldRenderSubMenu(action, action.children[0].dynamicList | async)">
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right left" (click)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventEvent($event)">
<button id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{action.title}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
<button *ngIf="willRenderAction(action)" id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{action.title}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>
</div>

View File

@ -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();
}

View File

@ -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();
}));

View File

@ -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) => {

View File

@ -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();
});

View File

@ -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();
}