diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 4635c3416..01f94feae 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -364,34 +364,6 @@ public class ReaderServiceTests _context.Series.Add(series); - // _context.Series.Add(new Series() - // { - // Name = "Test", - // NormalizedName = "Test".ToNormalized(), - // 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("2", 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" @@ -442,9 +414,6 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("21", actualChapter.Range); @@ -709,9 +678,6 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.NotEqual(-1, nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); @@ -743,8 +709,6 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.NotEqual(-1, nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); @@ -779,9 +743,6 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1); Assert.Equal(-1, nextChapter); } @@ -816,14 +777,48 @@ public class ReaderServiceTests await _context.SaveChangesAsync(); - - var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.NotEqual(-1, nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("B.cbz", actualChapter.Range); } + [Fact] + public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume_WhenAllVolumesHaveAChapterToo() + { + await ResetDb(); + + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("12").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("12").Build()) + .Build()) + .Build(); + series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + + _context.Series.Add(series); + + var user = new AppUserBuilder("majora2007", "fake").Build(); + + _context.AppUser.Add(user); + + await _context.SaveChangesAsync(); + + await _readerService.MarkChaptersAsRead(user, 1, new List() + { + series.Volumes.First().Chapters.First() + }); + + var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 1, 1); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); + Assert.Equal(2, actualChapter.Volume.Number); + } + #endregion #region GetPrevChapterIdAsync @@ -1281,6 +1276,38 @@ public class ReaderServiceTests var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("22", actualChapter.Range); } + + [Fact] + public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume_WhenAllVolumesHaveAChapterToo() + { + await ResetDb(); + + var series = new SeriesBuilder("Test") + .WithVolume(new VolumeBuilder("1") + .WithNumber(1) + .WithChapter(new ChapterBuilder("12").Build()) + .Build()) + + .WithVolume(new VolumeBuilder("2") + .WithNumber(2) + .WithChapter(new ChapterBuilder("12").Build()) + .Build()) + .Build(); + series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build(); + + _context.Series.Add(series); + + var user = new AppUserBuilder("majora2007", "fake").Build(); + + _context.AppUser.Add(user); + + await _context.SaveChangesAsync(); + + var nextChapter = await _readerService.GetPrevChapterIdAsync(1, 2, 2, 1); + var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter, ChapterIncludes.Volumes); + Assert.Equal(1, actualChapter.Volume.Number); + } + #endregion #region GetContinuePoint diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index a4f93b723..9f93eb033 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -111,13 +111,21 @@ public class LibraryController : BaseApiController return Ok(_directoryService.ListDirectory(path)); } - + /// + /// Return all libraries in the Server + /// + /// [HttpGet] public async Task>> GetLibraries() { return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername())); } + /// + /// For a given library, generate the jump bar information + /// + /// + /// [HttpGet("jump-bar")] public async Task>> GetJumpBar(int libraryId) { @@ -127,7 +135,11 @@ public class LibraryController : BaseApiController return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); } - + /// + /// Grants a user account access to a Library + /// + /// + /// [Authorize(Policy = "RequireAdminRole")] [HttpPost("grant-access")] public async Task> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) @@ -172,14 +184,34 @@ public class LibraryController : BaseApiController return BadRequest("There was a critical issue. Please try again."); } + /// + /// Scans a given library for file changes. + /// + /// + /// If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan + /// [Authorize(Policy = "RequireAdminRole")] [HttpPost("scan")] public ActionResult Scan(int libraryId, bool force = false) { + if (libraryId <= 0) return BadRequest("Invalid libraryId"); _taskScheduler.ScanLibrary(libraryId, force); return Ok(); } + /// + /// Scans a given library for file changes. If another scan task is in progress, will reschedule the invocation for 3 hours in future. + /// + /// If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("scan-all")] + public ActionResult ScanAll(bool force = false) + { + _taskScheduler.ScanLibraries(force); + return Ok(); + } + [Authorize(Policy = "RequireAdminRole")] [HttpPost("refresh-metadata")] public ActionResult RefreshMetadata(int libraryId, bool force = true) diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 1d4caef37..7d1d33396 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -156,6 +156,10 @@ public class ServerController : BaseApiController return Ok(); } + /// + /// Downloads all the log files via a zip + /// + /// [HttpGet("logs")] public ActionResult GetLogs() { @@ -180,6 +184,10 @@ public class ServerController : BaseApiController return Ok(await _versionUpdaterService.CheckForUpdate()); } + /// + /// Pull the Changelog for Kavita from Github and display + /// + /// [HttpGet("changelog")] public async Task>> GetChangelog() { @@ -198,6 +206,10 @@ public class ServerController : BaseApiController return Ok(await _accountService.CheckIfAccessible(Request)); } + /// + /// Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned. + /// + /// [HttpGet("jobs")] public ActionResult> GetJobs() { @@ -212,6 +224,7 @@ public class ServerController : BaseApiController }); return Ok(recurringJobs); - } + + } diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index dcaf31c62..7c99f6161 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -25,7 +25,6 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using VersOne.Epub; using VersOne.Epub.Options; -using Image = SixLabors.ImageSharp.Image; namespace API.Services; @@ -67,7 +66,7 @@ public class BookService : IBookService private const string BookApiUrl = "book-resources?file="; public static readonly EpubReaderOptions BookReaderOptions = new() { - PackageReaderOptions = new PackageReaderOptions() + PackageReaderOptions = new PackageReaderOptions { IgnoreMissingToc = true } @@ -194,7 +193,7 @@ public class BookService : IBookService var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty; var importBuilder = new StringBuilder(); //foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml)) - foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) + foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml)) { if (!match.Success) continue; @@ -246,7 +245,7 @@ public class BookService : IBookService private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend) { //foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex().Matches(stylesheetHtml)) - foreach (Match match in Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) + foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml)) { if (!match.Success) continue; var importFile = match.Groups["Filename"].Value; @@ -257,7 +256,7 @@ public class BookService : IBookService private static void EscapeFontFamilyReferences(ref string stylesheetHtml, string apiBase, string prepend) { //foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex().Matches(stylesheetHtml)) - foreach (Match match in Tasks.Scanner.Parser.Parser.FontSrcUrlRegex.Matches(stylesheetHtml)) + foreach (Match match in Parser.FontSrcUrlRegex.Matches(stylesheetHtml)) { if (!match.Success) continue; var importFile = match.Groups["Filename"].Value; @@ -268,7 +267,7 @@ public class BookService : IBookService private static void EscapeCssImageReferences(ref string stylesheetHtml, string apiBase, EpubBookRef book) { //var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex().Matches(stylesheetHtml); - var matches = Tasks.Scanner.Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml); + var matches = Parser.CssImageUrlRegex.Matches(stylesheetHtml); foreach (Match match in matches) { if (!match.Success) continue; @@ -424,7 +423,7 @@ public class BookService : IBookService public ComicInfo? GetComicInfo(string filePath) { - if (!IsValidFile(filePath) || Tasks.Scanner.Parser.Parser.IsPdf(filePath)) return null; + if (!IsValidFile(filePath) || Parser.IsPdf(filePath)) return null; try { @@ -438,10 +437,10 @@ public class BookService : IBookService } var (year, month, day) = GetPublicationDate(publicationDate); - var info = new ComicInfo() + var info = new ComicInfo { Summary = epubBook.Schema.Package.Metadata.Description, - Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Tasks.Scanner.Parser.Parser.CleanAuthor(c.Creator))), + Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.CleanAuthor(c.Creator))), Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers), Month = month, Day = day, @@ -489,14 +488,14 @@ public class BookService : IBookService } } - var hasVolumeInSeries = !Tasks.Scanner.Parser.Parser.ParseVolume(info.Title) - .Equals(Tasks.Scanner.Parser.Parser.DefaultVolume); + var hasVolumeInSeries = !Parser.ParseVolume(info.Title) + .Equals(Parser.DefaultVolume); if (string.IsNullOrEmpty(info.Volume) && hasVolumeInSeries && (!info.Series.Equals(info.Title) || string.IsNullOrEmpty(info.Series))) { // This is likely a light novel for which we can set series from parsed title - info.Series = Tasks.Scanner.Parser.Parser.ParseSeries(info.Title); - info.Volume = Tasks.Scanner.Parser.Parser.ParseVolume(info.Title); + info.Series = Parser.ParseSeries(info.Title); + info.Volume = Parser.ParseVolume(info.Title); } return info; @@ -552,7 +551,7 @@ public class BookService : IBookService return false; } - if (Tasks.Scanner.Parser.Parser.IsBook(filePath)) return true; + if (Parser.IsBook(filePath)) return true; _logger.LogWarning("[BookService] Book {EpubFile} is not a valid EPUB/PDF", filePath); return false; @@ -564,7 +563,7 @@ public class BookService : IBookService try { - if (Tasks.Scanner.Parser.Parser.IsPdf(filePath)) + if (Parser.IsPdf(filePath)) { using var docReader = DocLib.Instance.GetDocReader(filePath, new PageDimensions(1080, 1920)); return docReader.GetPageCount(); @@ -585,8 +584,8 @@ public class BookService : IBookService { // content = StartingScriptTag().Replace(content, ""); // content = StartingTitleTag().Replace(content, ""); - content = Regex.Replace(content, @")", "", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); - content = Regex.Replace(content, @")", "", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); + content = Regex.Replace(content, @")", "", RegexOptions.None, Parser.RegexTimeout); + content = Regex.Replace(content, @")", "", RegexOptions.None, Parser.RegexTimeout); return content; } @@ -622,7 +621,7 @@ public class BookService : IBookService /// public ParserInfo? ParseInfo(string filePath) { - if (!Tasks.Scanner.Parser.Parser.IsEpub(filePath)) return null; + if (!Parser.IsEpub(filePath)) return null; try { @@ -682,9 +681,9 @@ public class BookService : IBookService { specialName = epubBook.Title; } - var info = new ParserInfo() + var info = new ParserInfo { - Chapters = Tasks.Scanner.Parser.Parser.DefaultChapter, + Chapters = Parser.DefaultChapter, Edition = string.Empty, Format = MangaFormat.Epub, Filename = Path.GetFileName(filePath), @@ -704,9 +703,9 @@ public class BookService : IBookService // Swallow exception } - return new ParserInfo() + return new ParserInfo { - Chapters = Tasks.Scanner.Parser.Parser.DefaultChapter, + Chapters = Parser.DefaultChapter, Edition = string.Empty, Format = MangaFormat.Epub, Filename = Path.GetFileName(filePath), @@ -714,7 +713,7 @@ public class BookService : IBookService FullFilePath = filePath, IsSpecial = false, Series = epubBook.Title.Trim(), - Volumes = Tasks.Scanner.Parser.Parser.DefaultVolume, + Volumes = Parser.DefaultVolume, }; } catch (Exception ex) @@ -834,7 +833,7 @@ public class BookService : IBookService var key = CoalesceKey(book, mappings, nestedChapter.Link.ContentFileName); if (mappings.ContainsKey(key)) { - nestedChapters.Add(new BookChapterItem() + nestedChapters.Add(new BookChapterItem { Title = nestedChapter.Title, Page = mappings[key], @@ -871,7 +870,7 @@ public class BookService : IBookService { part = anchor.Attributes["href"].Value.Split("#")[1]; } - chaptersList.Add(new BookChapterItem() + chaptersList.Add(new BookChapterItem { Title = anchor.InnerText, Page = mappings[key], @@ -951,7 +950,7 @@ public class BookService : IBookService { if (navigationItem.Link == null) { - var item = new BookChapterItem() + var item = new BookChapterItem { Title = navigationItem.Title, Children = nestedChapters @@ -968,7 +967,7 @@ public class BookService : IBookService var groupKey = CleanContentKeys(navigationItem.Link.ContentFileName); if (mappings.ContainsKey(groupKey)) { - chaptersList.Add(new BookChapterItem() + chaptersList.Add(new BookChapterItem { Title = navigationItem.Title, Page = mappings[groupKey], @@ -991,7 +990,7 @@ public class BookService : IBookService { if (!IsValidFile(fileFilePath)) return string.Empty; - if (Tasks.Scanner.Parser.Parser.IsPdf(fileFilePath)) + if (Parser.IsPdf(fileFilePath)) { return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, saveAsWebP); } @@ -1002,7 +1001,7 @@ public class BookService : IBookService { // Try to get the cover image from OPF file, if not set, try to parse it from all the files, then result to the first one. var coverImageContent = epubBook.Content.Cover - ?? epubBook.Content.Images.Values.FirstOrDefault(file => Tasks.Scanner.Parser.Parser.IsCoverImage(file.FileName)) + ?? epubBook.Content.Images.Values.FirstOrDefault(file => Parser.IsCoverImage(file.FileName)) ?? epubBook.Content.Images.Values.FirstOrDefault(); if (coverImageContent == null) return string.Empty; @@ -1075,12 +1074,12 @@ public class BookService : IBookService // body = WhiteSpace2().Replace(body, string.Empty); // body = WhiteSpace3().Replace(body, " "); // body = WhiteSpace4().Replace(body, "$1"); - body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty, RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); + body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty, RegexOptions.None, Parser.RegexTimeout); - body = Regex.Replace(body, @"[a-zA-Z]+#", "#", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); - body = Regex.Replace(body, @"[\n\r]+\s*", string.Empty, RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); - body = Regex.Replace(body, @"\s+", " ", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); - body = Regex.Replace(body, @"\s?([:,;{}])\s?", "$1", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); + body = Regex.Replace(body, @"[a-zA-Z]+#", "#", RegexOptions.None, Parser.RegexTimeout); + body = Regex.Replace(body, @"[\n\r]+\s*", string.Empty, RegexOptions.None, Parser.RegexTimeout); + body = Regex.Replace(body, @"\s+", " ", RegexOptions.None, Parser.RegexTimeout); + body = Regex.Replace(body, @"\s?([:,;{}])\s?", "$1", RegexOptions.None, Parser.RegexTimeout); try { body = body.Replace(";}", "}"); @@ -1091,7 +1090,7 @@ public class BookService : IBookService } //body = UnitPadding().Replace(body, "$1"); - body = Regex.Replace(body, @"([\s:]0)(px|pt|%|em)", "$1", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); + body = Regex.Replace(body, @"([\s:]0)(px|pt|%|em)", "$1", RegexOptions.None, Parser.RegexTimeout); return body; diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 190407cb5..1b1327426 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -394,7 +394,7 @@ public class ReaderService : IReaderService var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number), currentChapter.Range, dto => dto.Range); if (chapterId > 0) return chapterId; - } else if (double.Parse(firstChapter.Number) > double.Parse(currentChapter.Number)) return firstChapter.Id; + } else if (double.Parse(firstChapter.Number) >= double.Parse(currentChapter.Number)) return firstChapter.Id; // If we are the last chapter and next volume is there, we should try to use it (unless it's volume 0) else if (double.Parse(firstChapter.Number) == 0) return firstChapter.Id; } diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs index df8b3023e..1ef67c1ce 100644 --- a/API/Services/ReadingItemService.cs +++ b/API/Services/ReadingItemService.cs @@ -39,12 +39,12 @@ public class ReadingItemService : IReadingItemService /// public ComicInfo? GetComicInfo(string filePath) { - if (Tasks.Scanner.Parser.Parser.IsEpub(filePath)) + if (Parser.IsEpub(filePath)) { return _bookService.GetComicInfo(filePath); } - if (Tasks.Scanner.Parser.Parser.IsComicInfoExtension(filePath)) + if (Parser.IsComicInfoExtension(filePath)) { return _archiveService.GetComicInfo(filePath); } @@ -68,18 +68,18 @@ public class ReadingItemService : IReadingItemService // This catches when original library type is Manga/Comic and when parsing with non - if (Tasks.Scanner.Parser.Parser.IsEpub(path) && Tasks.Scanner.Parser.Parser.ParseVolume(info.Series) != Tasks.Scanner.Parser.Parser.DefaultVolume) // Shouldn't this be info.Volume != DefaultVolume? + if (Parser.IsEpub(path) && Parser.ParseVolume(info.Series) != Parser.DefaultVolume) // Shouldn't this be info.Volume != DefaultVolume? { - var hasVolumeInTitle = !Tasks.Scanner.Parser.Parser.ParseVolume(info.Title) - .Equals(Tasks.Scanner.Parser.Parser.DefaultVolume); - var hasVolumeInSeries = !Tasks.Scanner.Parser.Parser.ParseVolume(info.Series) - .Equals(Tasks.Scanner.Parser.Parser.DefaultVolume); + var hasVolumeInTitle = !Parser.ParseVolume(info.Title) + .Equals(Parser.DefaultVolume); + var hasVolumeInSeries = !Parser.ParseVolume(info.Series) + .Equals(Parser.DefaultVolume); if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) { // This is likely a light novel for which we can set series from parsed title - info.Series = Tasks.Scanner.Parser.Parser.ParseSeries(info.Title); - info.Volumes = Tasks.Scanner.Parser.Parser.ParseVolume(info.Title); + info.Series = Parser.ParseSeries(info.Title); + info.Volumes = Parser.ParseVolume(info.Title); } else { @@ -111,11 +111,11 @@ public class ReadingItemService : IReadingItemService info.SeriesSort = info.ComicInfo.TitleSort.Trim(); } - if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Tasks.Scanner.Parser.Parser.HasComicInfoSpecial(info.ComicInfo.Format)) + if (!string.IsNullOrEmpty(info.ComicInfo.Format) && Parser.HasComicInfoSpecial(info.ComicInfo.Format)) { info.IsSpecial = true; - info.Chapters = Tasks.Scanner.Parser.Parser.DefaultChapter; - info.Volumes = Tasks.Scanner.Parser.Parser.DefaultVolume; + info.Chapters = Parser.DefaultChapter; + info.Volumes = Parser.DefaultVolume; } if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort)) @@ -216,6 +216,6 @@ public class ReadingItemService : IReadingItemService /// private ParserInfo? Parse(string path, string rootPath, LibraryType type) { - return Tasks.Scanner.Parser.Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type); + return Parser.IsEpub(path) ? _bookService.ParseInfo(path) : _defaultParser.Parse(path, rootPath, type); } } diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index 457348698..335370c98 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -21,6 +21,7 @@ public interface ITaskScheduler void ScanFolder(string folderPath, TimeSpan delay); void ScanFolder(string folderPath); void ScanLibrary(int libraryId, bool force = false); + void ScanLibraries(bool force = false); void CleanupChapters(int[] chapterIds); void RefreshMetadata(int libraryId, bool forceUpdate = true); void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false); @@ -32,6 +33,7 @@ public interface ITaskScheduler void ScanSiteThemes(); Task CovertAllCoversToWebP(); Task CleanupDbEntries(); + } public class TaskScheduler : ITaskScheduler { @@ -97,12 +99,12 @@ public class TaskScheduler : ITaskScheduler { var scanLibrarySetting = setting; _logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting); - RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => _scannerService.ScanLibraries(), + RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => _scannerService.ScanLibraries(false), () => CronConverter.ConvertToCronNotation(scanLibrarySetting), TimeZoneInfo.Local); } else { - RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(), Cron.Daily, TimeZoneInfo.Local); + RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, TimeZoneInfo.Local); } setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value; @@ -237,15 +239,19 @@ public class TaskScheduler : ITaskScheduler await _cleanupService.CleanupDbEntries(); } - public void ScanLibraries() + /// + /// Attempts to call ScanLibraries on ScannerService, but if another scan task is in progress, will reschedule the invocation for 3 hours in future. + /// + /// + public void ScanLibraries(bool force = false) { if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) { _logger.LogInformation("A Scan is already running, rescheduling ScanLibraries in 3 hours"); - BackgroundJob.Schedule(() => ScanLibraries(), TimeSpan.FromHours(3)); + BackgroundJob.Schedule(() => ScanLibraries(force), TimeSpan.FromHours(3)); return; } - _scannerService.ScanLibraries(); + _scannerService.ScanLibraries(force); } public void ScanLibrary(int libraryId, bool force = false) diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 18c3e2d16..f50fd778f 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -834,15 +834,12 @@ public class ProcessSeries : IProcessSeries var normalizedName = name.ToNormalized(); var person = allPeopleTypeRole.FirstOrDefault(p => p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); - _logger.LogTrace("[UpdatePeople] Checking if we can add {Name} for {Role}", names, role); if (person == null) { person = new PersonBuilder(name, role).Build(); - _logger.LogTrace("[UpdatePeople] for {Role} no one found, adding to _people", role); _people.Add(person); } - _logger.LogTrace("[UpdatePeople] For {Name}, found person with id: {Id}", role, person.Id); action(person); } } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 492107829..7c3a6abf5 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -35,7 +35,7 @@ public interface IScannerService [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - Task ScanLibraries(); + Task ScanLibraries(bool forceUpdate = false); [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] @@ -439,12 +439,12 @@ public class ScannerService : IScannerService [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] - public async Task ScanLibraries() + public async Task ScanLibraries(bool forceUpdate = false) { _logger.LogInformation("Starting Scan of All Libraries"); foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync()) { - await ScanLibrary(lib.Id); + await ScanLibrary(lib.Id, forceUpdate); } _logger.LogInformation("Scan of All Libraries Finished"); } diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html index c86087667..cb0997bc0 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html @@ -27,6 +27,20 @@ +
+
+   + Location where bookmarks will be stored. Bookmarks are source files and can be large. Choose a location with adequate storage. Directory is managed; other files within directory will be deleted. If Docker, mount an additional volume and use that. + +
+ + +
+
+
+
diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts index 12a97481c..e7153b6f1 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts @@ -4,6 +4,8 @@ import { ToastrService } from 'ngx-toastr'; import { take } from 'rxjs'; import { SettingsService } from '../settings.service'; import { ServerSettings } from '../_models/server-settings'; +import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'app-manage-media-settings', @@ -15,23 +17,25 @@ export class ManageMediaSettingsComponent implements OnInit { serverSettings!: ServerSettings; settingsForm: FormGroup = new FormGroup({}); - constructor(private settingsService: SettingsService, private toastr: ToastrService) { } + constructor(private settingsService: SettingsService, private toastr: ToastrService, private modalService: NgbModal, ) { } ngOnInit(): void { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, [Validators.required])); this.settingsForm.addControl('convertCoverToWebP', new FormControl(this.serverSettings.convertCoverToWebP, [Validators.required])); + this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required])); }); } resetForm() { this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP); this.settingsForm.get('convertCoverToWebP')?.setValue(this.serverSettings.convertCoverToWebP); + this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory); this.settingsForm.markAsPristine(); } - async saveSettings() { + saveSettings() { const modelSettings = Object.assign({}, this.serverSettings); modelSettings.convertBookmarkToWebP = this.settingsForm.get('convertBookmarkToWebP')?.value; modelSettings.convertCoverToWebP = this.settingsForm.get('convertCoverToWebP')?.value; @@ -46,7 +50,7 @@ export class ManageMediaSettingsComponent implements OnInit { } resetToDefaults() { - this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => { + this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); this.toastr.success('Server settings updated'); @@ -54,4 +58,16 @@ export class ManageMediaSettingsComponent implements OnInit { console.error('error: ', err); }); } + + openDirectoryChooser(existingDirectory: string, formControl: string) { + const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg' }); + modalRef.componentInstance.startingFolder = existingDirectory || ''; + modalRef.componentInstance.helpUrl = ''; + modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => { + if (closeResult.success && closeResult.folderPath !== '') { + this.settingsForm.get(formControl)?.setValue(closeResult.folderPath); + this.settingsForm.markAsDirty(); + } + }); + } } diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 3087dee28..3acffc643 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -10,19 +10,6 @@
--> -
-   - Location where bookmarks will be stored. Bookmarks are source files and can be large. Choose a location with adequate storage. Directory is managed; other files within directory will be deleted. If Docker, mount an additional volume and use that. - -
- - -
-
- -
  Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user. diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 9b733bb65..682a9ddce 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -1,12 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { FormGroup, Validators, FormControl } from '@angular/forms'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { take } from 'rxjs/operators'; import { TagBadgeCursor } from 'src/app/shared/tag-badge/tag-badge.component'; import { ServerService } from 'src/app/_services/server.service'; import { SettingsService } from '../settings.service'; -import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component'; import { ServerSettings } from '../_models/server-settings'; const ValidIpAddress = /^(\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*\,)*\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*$/i; @@ -28,7 +26,7 @@ export class ManageSettingsComponent implements OnInit { } constructor(private settingsService: SettingsService, private toastr: ToastrService, - private modalService: NgbModal, private serverService: ServerService) { } + private serverService: ServerService) { } ngOnInit(): void { this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => { @@ -40,7 +38,6 @@ export class ManageSettingsComponent implements OnInit { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required])); - this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required])); this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required])); this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required])); this.settingsForm.addControl('ipAddresses', new FormControl(this.serverSettings.ipAddresses, [Validators.required, Validators.pattern(ValidIpAddress)])); @@ -67,7 +64,6 @@ export class ManageSettingsComponent implements OnInit { resetForm() { this.settingsForm.get('cacheDirectory')?.setValue(this.serverSettings.cacheDirectory); - this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory); this.settingsForm.get('scanTask')?.setValue(this.serverSettings.taskScan); this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup); this.settingsForm.get('ipAddresses')?.setValue(this.serverSettings.ipAddresses); @@ -127,15 +123,5 @@ export class ManageSettingsComponent implements OnInit { }); } - openDirectoryChooser(existingDirectory: string, formControl: string) { - const modalRef = this.modalService.open(DirectoryPickerComponent, { scrollable: true, size: 'lg' }); - modalRef.componentInstance.startingFolder = existingDirectory || ''; - modalRef.componentInstance.helpUrl = ''; - modalRef.closed.subscribe((closeResult: DirectoryPickerResult) => { - if (closeResult.success && closeResult.folderPath !== '') { - this.settingsForm.get(formControl)?.setValue(closeResult.folderPath); - this.settingsForm.markAsDirty(); - } - }); - } + } diff --git a/UI/Web/src/app/registration/_components/register/register.component.html b/UI/Web/src/app/registration/_components/register/register.component.html index bce6dd5bb..bd68668b2 100644 --- a/UI/Web/src/app/registration/_components/register/register.component.html +++ b/UI/Web/src/app/registration/_components/register/register.component.html @@ -51,7 +51,7 @@
- +
diff --git a/UI/Web/src/app/registration/_components/register/register.component.ts b/UI/Web/src/app/registration/_components/register/register.component.ts index 35d0e852d..84c1b0410 100644 --- a/UI/Web/src/app/registration/_components/register/register.component.ts +++ b/UI/Web/src/app/registration/_components/register/register.component.ts @@ -18,7 +18,7 @@ import { MemberService } from 'src/app/_services/member.service'; export class RegisterComponent { registerForm: FormGroup = new FormGroup({ - email: new FormControl('', [Validators.email]), + email: new FormControl('', [Validators.required]), username: new FormControl('', [Validators.required]), password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]), }); diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts index 6c1baf1a5..56686100c 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.ts @@ -257,10 +257,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy { transformKeyToOpdsUrl(key: string) { if (environment.production) { - return `${location.origin}${environment.apiUrl}opds/${key}`.replace('//', '/'); + return `${location.origin}` + `${this.baseUrl}${environment.apiUrl}opds/${key}`.replace('//', '/'); } - return `${location.origin}${this.baseUrl}api/opds/${key}`.replace('//', '/'); + return `${location.origin}${this.baseUrl.replace('//', '/')}api/opds/${key}`; } handleBackgroundColorChange() { diff --git a/openapi.json b/openapi.json index b0f3dfcee..8e9dd72f5 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.1.41" + "version": "0.7.1.43" }, "servers": [ { @@ -2175,6 +2175,7 @@ "tags": [ "Library" ], + "summary": "Return all libraries in the Server", "responses": { "200": { "description": "Success", @@ -2213,10 +2214,12 @@ "tags": [ "Library" ], + "summary": "For a given library, generate the jump bar information", "parameters": [ { "name": "libraryId", "in": "query", + "description": "", "schema": { "type": "integer", "format": "int32" @@ -2261,7 +2264,9 @@ "tags": [ "Library" ], + "summary": "Grants a user account access to a Library", "requestBody": { + "description": "", "content": { "application/json": { "schema": { @@ -2309,10 +2314,12 @@ "tags": [ "Library" ], + "summary": "Scans a given library for file changes.", "parameters": [ { "name": "libraryId", "in": "query", + "description": "", "schema": { "type": "integer", "format": "int32" @@ -2321,6 +2328,31 @@ { "name": "force", "in": "query", + "description": "If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/Library/scan-all": { + "post": { + "tags": [ + "Library" + ], + "summary": "Scans a given library for file changes. If another scan task is in progress, will reschedule the invocation for 3 hours in future.", + "parameters": [ + { + "name": "force", + "in": "query", + "description": "If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan", "schema": { "type": "boolean", "default": false @@ -7463,6 +7495,7 @@ "tags": [ "Server" ], + "summary": "Downloads all the log files via a zip", "responses": { "200": { "description": "Success" @@ -7505,6 +7538,7 @@ "tags": [ "Server" ], + "summary": "Pull the Changelog for Kavita from Github and display", "responses": { "200": { "description": "Success", @@ -7574,6 +7608,7 @@ "tags": [ "Server" ], + "summary": "Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned.", "responses": { "200": { "description": "Success",