mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
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:
parent
b6f6b0ed99
commit
c652c36081
@ -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));
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -24,6 +24,7 @@ public class ThemeController : BaseApiController
|
||||
_taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
[ResponseCache(CacheProfileName = "10Minute")]
|
||||
[AllowAnonymous]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<SiteThemeDto>>> GetThemes()
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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()));
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}));
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user