Release Testing Day 3 (#1946)

* Removed extra trace messages as the people issue might have been resolved.

* When registering, disable button until form is valid. Allow non-email formatted emails, but not blank.

* Fixed opds not having http(s)://

* Added a new API to allow scanning all libraries from end point

* Moved Bookmarks directory to Media tab

* Fixed an edge case for finding next chapter when we had volume 1,2 etc but they had the same chapter number.

* Code cleanup
This commit is contained in:
Joe Milazzo 2023-04-29 07:49:00 -05:00 committed by GitHub
parent 119ea35b62
commit 4e0e3608aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 253 additions and 141 deletions

View File

@ -364,34 +364,6 @@ public class ReaderServiceTests
_context.Series.Add(series); _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<Volume>()
// {
// EntityFactory.CreateVolume("1", new List<Chapter>()
// {
// EntityFactory.CreateChapter("1", false, new List<MangaFile>()),
// EntityFactory.CreateChapter("2", false, new List<MangaFile>()),
// }),
// EntityFactory.CreateVolume("2", 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() _context.AppUser.Add(new AppUser()
{ {
UserName = "majora2007" UserName = "majora2007"
@ -442,9 +414,6 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("21", actualChapter.Range); Assert.Equal("21", actualChapter.Range);
@ -709,9 +678,6 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
@ -743,8 +709,6 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 2, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
@ -779,9 +743,6 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 1, 3, 1);
Assert.Equal(-1, nextChapter); Assert.Equal(-1, nextChapter);
} }
@ -816,14 +777,48 @@ public class ReaderServiceTests
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1); var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 3, 1);
Assert.NotEqual(-1, nextChapter); Assert.NotEqual(-1, nextChapter);
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
Assert.Equal("B.cbz", actualChapter.Range); 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<Chapter>()
{
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 #endregion
#region GetPrevChapterIdAsync #region GetPrevChapterIdAsync
@ -1281,6 +1276,38 @@ public class ReaderServiceTests
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter);
Assert.Equal("22", actualChapter.Range); 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 #endregion
#region GetContinuePoint #region GetContinuePoint

View File

@ -111,13 +111,21 @@ public class LibraryController : BaseApiController
return Ok(_directoryService.ListDirectory(path)); return Ok(_directoryService.ListDirectory(path));
} }
/// <summary>
/// Return all libraries in the Server
/// </summary>
/// <returns></returns>
[HttpGet] [HttpGet]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries() public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{ {
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername())); return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(User.GetUsername()));
} }
/// <summary>
/// For a given library, generate the jump bar information
/// </summary>
/// <param name="libraryId"></param>
/// <returns></returns>
[HttpGet("jump-bar")] [HttpGet("jump-bar")]
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId) public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
{ {
@ -127,7 +135,11 @@ public class LibraryController : BaseApiController
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId)); return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
} }
/// <summary>
/// Grants a user account access to a Library
/// </summary>
/// <param name="updateLibraryForUserDto"></param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpPost("grant-access")] [HttpPost("grant-access")]
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto) public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
@ -172,14 +184,34 @@ public class LibraryController : BaseApiController
return BadRequest("There was a critical issue. Please try again."); return BadRequest("There was a critical issue. Please try again.");
} }
/// <summary>
/// Scans a given library for file changes.
/// </summary>
/// <param name="libraryId"></param>
/// <param name="force">If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan")] [HttpPost("scan")]
public ActionResult Scan(int libraryId, bool force = false) public ActionResult Scan(int libraryId, bool force = false)
{ {
if (libraryId <= 0) return BadRequest("Invalid libraryId");
_taskScheduler.ScanLibrary(libraryId, force); _taskScheduler.ScanLibrary(libraryId, force);
return Ok(); return Ok();
} }
/// <summary>
/// Scans a given library for file changes. If another scan task is in progress, will reschedule the invocation for 3 hours in future.
/// </summary>
/// <param name="force">If true, will ignore any optimizations to avoid file I/O and will treat similar to a first scan</param>
/// <returns></returns>
[Authorize(Policy = "RequireAdminRole")]
[HttpPost("scan-all")]
public ActionResult ScanAll(bool force = false)
{
_taskScheduler.ScanLibraries(force);
return Ok();
}
[Authorize(Policy = "RequireAdminRole")] [Authorize(Policy = "RequireAdminRole")]
[HttpPost("refresh-metadata")] [HttpPost("refresh-metadata")]
public ActionResult RefreshMetadata(int libraryId, bool force = true) public ActionResult RefreshMetadata(int libraryId, bool force = true)

View File

@ -156,6 +156,10 @@ public class ServerController : BaseApiController
return Ok(); return Ok();
} }
/// <summary>
/// Downloads all the log files via a zip
/// </summary>
/// <returns></returns>
[HttpGet("logs")] [HttpGet("logs")]
public ActionResult GetLogs() public ActionResult GetLogs()
{ {
@ -180,6 +184,10 @@ public class ServerController : BaseApiController
return Ok(await _versionUpdaterService.CheckForUpdate()); return Ok(await _versionUpdaterService.CheckForUpdate());
} }
/// <summary>
/// Pull the Changelog for Kavita from Github and display
/// </summary>
/// <returns></returns>
[HttpGet("changelog")] [HttpGet("changelog")]
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog() public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
{ {
@ -198,6 +206,10 @@ public class ServerController : BaseApiController
return Ok(await _accountService.CheckIfAccessible(Request)); return Ok(await _accountService.CheckIfAccessible(Request));
} }
/// <summary>
/// Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned.
/// </summary>
/// <returns></returns>
[HttpGet("jobs")] [HttpGet("jobs")]
public ActionResult<IEnumerable<JobDto>> GetJobs() public ActionResult<IEnumerable<JobDto>> GetJobs()
{ {
@ -212,6 +224,7 @@ public class ServerController : BaseApiController
}); });
return Ok(recurringJobs); return Ok(recurringJobs);
} }
} }

View File

@ -25,7 +25,6 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using VersOne.Epub; using VersOne.Epub;
using VersOne.Epub.Options; using VersOne.Epub.Options;
using Image = SixLabors.ImageSharp.Image;
namespace API.Services; namespace API.Services;
@ -67,7 +66,7 @@ public class BookService : IBookService
private const string BookApiUrl = "book-resources?file="; private const string BookApiUrl = "book-resources?file=";
public static readonly EpubReaderOptions BookReaderOptions = new() public static readonly EpubReaderOptions BookReaderOptions = new()
{ {
PackageReaderOptions = new PackageReaderOptions() PackageReaderOptions = new PackageReaderOptions
{ {
IgnoreMissingToc = true 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 prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty;
var importBuilder = new StringBuilder(); 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 Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml))
{ {
if (!match.Success) continue; if (!match.Success) continue;
@ -246,7 +245,7 @@ public class BookService : IBookService
private static void EscapeCssImportReferences(ref string stylesheetHtml, string apiBase, string prepend) 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 Tasks.Scanner.Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml)) foreach (Match match in Parser.CssImportUrlRegex.Matches(stylesheetHtml))
{ {
if (!match.Success) continue; if (!match.Success) continue;
var importFile = match.Groups["Filename"].Value; 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) 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 Tasks.Scanner.Parser.Parser.FontSrcUrlRegex.Matches(stylesheetHtml)) foreach (Match match in Parser.FontSrcUrlRegex.Matches(stylesheetHtml))
{ {
if (!match.Success) continue; if (!match.Success) continue;
var importFile = match.Groups["Filename"].Value; 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) 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 = Tasks.Scanner.Parser.Parser.CssImageUrlRegex.Matches(stylesheetHtml); var matches = Parser.CssImageUrlRegex.Matches(stylesheetHtml);
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (!match.Success) continue; if (!match.Success) continue;
@ -424,7 +423,7 @@ public class BookService : IBookService
public ComicInfo? GetComicInfo(string filePath) 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 try
{ {
@ -438,10 +437,10 @@ public class BookService : IBookService
} }
var (year, month, day) = GetPublicationDate(publicationDate); var (year, month, day) = GetPublicationDate(publicationDate);
var info = new ComicInfo() var info = new ComicInfo
{ {
Summary = epubBook.Schema.Package.Metadata.Description, 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), Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers),
Month = month, Month = month,
Day = day, Day = day,
@ -489,14 +488,14 @@ public class BookService : IBookService
} }
} }
var hasVolumeInSeries = !Tasks.Scanner.Parser.Parser.ParseVolume(info.Title) var hasVolumeInSeries = !Parser.ParseVolume(info.Title)
.Equals(Tasks.Scanner.Parser.Parser.DefaultVolume); .Equals(Parser.DefaultVolume);
if (string.IsNullOrEmpty(info.Volume) && hasVolumeInSeries && (!info.Series.Equals(info.Title) || string.IsNullOrEmpty(info.Series))) 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 // 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.Series = Parser.ParseSeries(info.Title);
info.Volume = Tasks.Scanner.Parser.Parser.ParseVolume(info.Title); info.Volume = Parser.ParseVolume(info.Title);
} }
return info; return info;
@ -552,7 +551,7 @@ public class BookService : IBookService
return false; 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); _logger.LogWarning("[BookService] Book {EpubFile} is not a valid EPUB/PDF", filePath);
return false; return false;
@ -564,7 +563,7 @@ public class BookService : IBookService
try try
{ {
if (Tasks.Scanner.Parser.Parser.IsPdf(filePath)) if (Parser.IsPdf(filePath))
{ {
using var docReader = DocLib.Instance.GetDocReader(filePath, new PageDimensions(1080, 1920)); using var docReader = DocLib.Instance.GetDocReader(filePath, new PageDimensions(1080, 1920));
return docReader.GetPageCount(); return docReader.GetPageCount();
@ -585,8 +584,8 @@ public class BookService : IBookService
{ {
// content = StartingScriptTag().Replace(content, "<script$1></script>"); // content = StartingScriptTag().Replace(content, "<script$1></script>");
// content = StartingTitleTag().Replace(content, "<title$1></title>"); // content = StartingTitleTag().Replace(content, "<title$1></title>");
content = Regex.Replace(content, @"<script(.*)(/>)", "<script$1></script>", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); content = Regex.Replace(content, @"<script(.*)(/>)", "<script$1></script>", RegexOptions.None, Parser.RegexTimeout);
content = Regex.Replace(content, @"<title(.*)(/>)", "<title$1></title>", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); content = Regex.Replace(content, @"<title(.*)(/>)", "<title$1></title>", RegexOptions.None, Parser.RegexTimeout);
return content; return content;
} }
@ -622,7 +621,7 @@ public class BookService : IBookService
/// <returns></returns> /// <returns></returns>
public ParserInfo? ParseInfo(string filePath) public ParserInfo? ParseInfo(string filePath)
{ {
if (!Tasks.Scanner.Parser.Parser.IsEpub(filePath)) return null; if (!Parser.IsEpub(filePath)) return null;
try try
{ {
@ -682,9 +681,9 @@ public class BookService : IBookService
{ {
specialName = epubBook.Title; specialName = epubBook.Title;
} }
var info = new ParserInfo() var info = new ParserInfo
{ {
Chapters = Tasks.Scanner.Parser.Parser.DefaultChapter, Chapters = Parser.DefaultChapter,
Edition = string.Empty, Edition = string.Empty,
Format = MangaFormat.Epub, Format = MangaFormat.Epub,
Filename = Path.GetFileName(filePath), Filename = Path.GetFileName(filePath),
@ -704,9 +703,9 @@ public class BookService : IBookService
// Swallow exception // Swallow exception
} }
return new ParserInfo() return new ParserInfo
{ {
Chapters = Tasks.Scanner.Parser.Parser.DefaultChapter, Chapters = Parser.DefaultChapter,
Edition = string.Empty, Edition = string.Empty,
Format = MangaFormat.Epub, Format = MangaFormat.Epub,
Filename = Path.GetFileName(filePath), Filename = Path.GetFileName(filePath),
@ -714,7 +713,7 @@ public class BookService : IBookService
FullFilePath = filePath, FullFilePath = filePath,
IsSpecial = false, IsSpecial = false,
Series = epubBook.Title.Trim(), Series = epubBook.Title.Trim(),
Volumes = Tasks.Scanner.Parser.Parser.DefaultVolume, Volumes = Parser.DefaultVolume,
}; };
} }
catch (Exception ex) catch (Exception ex)
@ -834,7 +833,7 @@ public class BookService : IBookService
var key = CoalesceKey(book, mappings, nestedChapter.Link.ContentFileName); var key = CoalesceKey(book, mappings, nestedChapter.Link.ContentFileName);
if (mappings.ContainsKey(key)) if (mappings.ContainsKey(key))
{ {
nestedChapters.Add(new BookChapterItem() nestedChapters.Add(new BookChapterItem
{ {
Title = nestedChapter.Title, Title = nestedChapter.Title,
Page = mappings[key], Page = mappings[key],
@ -871,7 +870,7 @@ public class BookService : IBookService
{ {
part = anchor.Attributes["href"].Value.Split("#")[1]; part = anchor.Attributes["href"].Value.Split("#")[1];
} }
chaptersList.Add(new BookChapterItem() chaptersList.Add(new BookChapterItem
{ {
Title = anchor.InnerText, Title = anchor.InnerText,
Page = mappings[key], Page = mappings[key],
@ -951,7 +950,7 @@ public class BookService : IBookService
{ {
if (navigationItem.Link == null) if (navigationItem.Link == null)
{ {
var item = new BookChapterItem() var item = new BookChapterItem
{ {
Title = navigationItem.Title, Title = navigationItem.Title,
Children = nestedChapters Children = nestedChapters
@ -968,7 +967,7 @@ public class BookService : IBookService
var groupKey = CleanContentKeys(navigationItem.Link.ContentFileName); var groupKey = CleanContentKeys(navigationItem.Link.ContentFileName);
if (mappings.ContainsKey(groupKey)) if (mappings.ContainsKey(groupKey))
{ {
chaptersList.Add(new BookChapterItem() chaptersList.Add(new BookChapterItem
{ {
Title = navigationItem.Title, Title = navigationItem.Title,
Page = mappings[groupKey], Page = mappings[groupKey],
@ -991,7 +990,7 @@ public class BookService : IBookService
{ {
if (!IsValidFile(fileFilePath)) return string.Empty; if (!IsValidFile(fileFilePath)) return string.Empty;
if (Tasks.Scanner.Parser.Parser.IsPdf(fileFilePath)) if (Parser.IsPdf(fileFilePath))
{ {
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, saveAsWebP); 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. // 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 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(); ?? epubBook.Content.Images.Values.FirstOrDefault();
if (coverImageContent == null) return string.Empty; if (coverImageContent == null) return string.Empty;
@ -1075,12 +1074,12 @@ public class BookService : IBookService
// body = WhiteSpace2().Replace(body, string.Empty); // body = WhiteSpace2().Replace(body, string.Empty);
// body = WhiteSpace3().Replace(body, " "); // body = WhiteSpace3().Replace(body, " ");
// body = WhiteSpace4().Replace(body, "$1"); // 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, @"[a-zA-Z]+#", "#", RegexOptions.None, Parser.RegexTimeout);
body = Regex.Replace(body, @"[\n\r]+\s*", string.Empty, RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); body = Regex.Replace(body, @"[\n\r]+\s*", string.Empty, RegexOptions.None, Parser.RegexTimeout);
body = Regex.Replace(body, @"\s+", " ", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); body = Regex.Replace(body, @"\s+", " ", RegexOptions.None, Parser.RegexTimeout);
body = Regex.Replace(body, @"\s?([:,;{}])\s?", "$1", RegexOptions.None, Tasks.Scanner.Parser.Parser.RegexTimeout); body = Regex.Replace(body, @"\s?([:,;{}])\s?", "$1", RegexOptions.None, Parser.RegexTimeout);
try try
{ {
body = body.Replace(";}", "}"); body = body.Replace(";}", "}");
@ -1091,7 +1090,7 @@ public class BookService : IBookService
} }
//body = UnitPadding().Replace(body, "$1"); //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; return body;

View File

@ -394,7 +394,7 @@ public class ReaderService : IReaderService
var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number), var chapterId = GetNextChapterId(volume.Chapters.OrderByNatural(x => x.Number),
currentChapter.Range, dto => dto.Range); currentChapter.Range, dto => dto.Range);
if (chapterId > 0) return chapterId; 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) // 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; else if (double.Parse(firstChapter.Number) == 0) return firstChapter.Id;
} }

View File

@ -39,12 +39,12 @@ public class ReadingItemService : IReadingItemService
/// <returns></returns> /// <returns></returns>
public ComicInfo? GetComicInfo(string filePath) public ComicInfo? GetComicInfo(string filePath)
{ {
if (Tasks.Scanner.Parser.Parser.IsEpub(filePath)) if (Parser.IsEpub(filePath))
{ {
return _bookService.GetComicInfo(filePath); return _bookService.GetComicInfo(filePath);
} }
if (Tasks.Scanner.Parser.Parser.IsComicInfoExtension(filePath)) if (Parser.IsComicInfoExtension(filePath))
{ {
return _archiveService.GetComicInfo(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 // 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) var hasVolumeInTitle = !Parser.ParseVolume(info.Title)
.Equals(Tasks.Scanner.Parser.Parser.DefaultVolume); .Equals(Parser.DefaultVolume);
var hasVolumeInSeries = !Tasks.Scanner.Parser.Parser.ParseVolume(info.Series) var hasVolumeInSeries = !Parser.ParseVolume(info.Series)
.Equals(Tasks.Scanner.Parser.Parser.DefaultVolume); .Equals(Parser.DefaultVolume);
if (string.IsNullOrEmpty(info.ComicInfo?.Volume) && hasVolumeInTitle && (hasVolumeInSeries || string.IsNullOrEmpty(info.Series))) 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 // 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.Series = Parser.ParseSeries(info.Title);
info.Volumes = Tasks.Scanner.Parser.Parser.ParseVolume(info.Title); info.Volumes = Parser.ParseVolume(info.Title);
} }
else else
{ {
@ -111,11 +111,11 @@ public class ReadingItemService : IReadingItemService
info.SeriesSort = info.ComicInfo.TitleSort.Trim(); 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.IsSpecial = true;
info.Chapters = Tasks.Scanner.Parser.Parser.DefaultChapter; info.Chapters = Parser.DefaultChapter;
info.Volumes = Tasks.Scanner.Parser.Parser.DefaultVolume; info.Volumes = Parser.DefaultVolume;
} }
if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort)) if (!string.IsNullOrEmpty(info.ComicInfo.SeriesSort))
@ -216,6 +216,6 @@ public class ReadingItemService : IReadingItemService
/// <returns></returns> /// <returns></returns>
private ParserInfo? Parse(string path, string rootPath, LibraryType type) 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);
} }
} }

View File

@ -21,6 +21,7 @@ public interface ITaskScheduler
void ScanFolder(string folderPath, TimeSpan delay); void ScanFolder(string folderPath, TimeSpan delay);
void ScanFolder(string folderPath); void ScanFolder(string folderPath);
void ScanLibrary(int libraryId, bool force = false); void ScanLibrary(int libraryId, bool force = false);
void ScanLibraries(bool force = false);
void CleanupChapters(int[] chapterIds); void CleanupChapters(int[] chapterIds);
void RefreshMetadata(int libraryId, bool forceUpdate = true); void RefreshMetadata(int libraryId, bool forceUpdate = true);
void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false); void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false);
@ -32,6 +33,7 @@ public interface ITaskScheduler
void ScanSiteThemes(); void ScanSiteThemes();
Task CovertAllCoversToWebP(); Task CovertAllCoversToWebP();
Task CleanupDbEntries(); Task CleanupDbEntries();
} }
public class TaskScheduler : ITaskScheduler public class TaskScheduler : ITaskScheduler
{ {
@ -97,12 +99,12 @@ public class TaskScheduler : ITaskScheduler
{ {
var scanLibrarySetting = setting; var scanLibrarySetting = setting;
_logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting); _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); () => CronConverter.ConvertToCronNotation(scanLibrarySetting), TimeZoneInfo.Local);
} }
else 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; setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value;
@ -237,15 +239,19 @@ public class TaskScheduler : ITaskScheduler
await _cleanupService.CleanupDbEntries(); await _cleanupService.CleanupDbEntries();
} }
public void ScanLibraries() /// <summary>
/// Attempts to call ScanLibraries on ScannerService, but if another scan task is in progress, will reschedule the invocation for 3 hours in future.
/// </summary>
/// <param name="force"></param>
public void ScanLibraries(bool force = false)
{ {
if (RunningAnyTasksByMethod(ScanTasks, ScanQueue)) if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{ {
_logger.LogInformation("A Scan is already running, rescheduling ScanLibraries in 3 hours"); _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; return;
} }
_scannerService.ScanLibraries(); _scannerService.ScanLibraries(force);
} }
public void ScanLibrary(int libraryId, bool force = false) public void ScanLibrary(int libraryId, bool force = false)

View File

@ -834,15 +834,12 @@ public class ProcessSeries : IProcessSeries
var normalizedName = name.ToNormalized(); var normalizedName = name.ToNormalized();
var person = allPeopleTypeRole.FirstOrDefault(p => var person = allPeopleTypeRole.FirstOrDefault(p =>
p.NormalizedName != null && p.NormalizedName.Equals(normalizedName)); p.NormalizedName != null && p.NormalizedName.Equals(normalizedName));
_logger.LogTrace("[UpdatePeople] Checking if we can add {Name} for {Role}", names, role);
if (person == null) if (person == null)
{ {
person = new PersonBuilder(name, role).Build(); person = new PersonBuilder(name, role).Build();
_logger.LogTrace("[UpdatePeople] for {Role} no one found, adding to _people", role);
_people.Add(person); _people.Add(person);
} }
_logger.LogTrace("[UpdatePeople] For {Name}, found person with id: {Id}", role, person.Id);
action(person); action(person);
} }
} }

View File

@ -35,7 +35,7 @@ public interface IScannerService
[Queue(TaskScheduler.ScanQueue)] [Queue(TaskScheduler.ScanQueue)]
[DisableConcurrentExecution(60 * 60 * 60)] [DisableConcurrentExecution(60 * 60 * 60)]
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
Task ScanLibraries(); Task ScanLibraries(bool forceUpdate = false);
[Queue(TaskScheduler.ScanQueue)] [Queue(TaskScheduler.ScanQueue)]
[DisableConcurrentExecution(60 * 60 * 60)] [DisableConcurrentExecution(60 * 60 * 60)]
@ -439,12 +439,12 @@ public class ScannerService : IScannerService
[Queue(TaskScheduler.ScanQueue)] [Queue(TaskScheduler.ScanQueue)]
[DisableConcurrentExecution(60 * 60 * 60)] [DisableConcurrentExecution(60 * 60 * 60)]
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ScanLibraries() public async Task ScanLibraries(bool forceUpdate = false)
{ {
_logger.LogInformation("Starting Scan of All Libraries"); _logger.LogInformation("Starting Scan of All Libraries");
foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync()) foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync())
{ {
await ScanLibrary(lib.Id); await ScanLibrary(lib.Id, forceUpdate);
} }
_logger.LogInformation("Scan of All Libraries Finished"); _logger.LogInformation("Scan of All Libraries Finished");
} }

View File

@ -27,6 +27,20 @@
</div> </div>
</div> </div>
<div class="row g-0">
<div class="mb-3">
<label for="settings-bookmarksdir" class="form-label">Bookmarks Directory</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
<ng-template #bookmarksDirectoryTooltip>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.</ng-template>
<span class="visually-hidden" id="settings-bookmarksdir-help"><ng-container [ngTemplateOutlet]="bookmarksDirectoryTooltip"></ng-container></span>
<div class="input-group">
<input readonly id="settings-bookmarksdir" aria-describedby="settings-bookmarksdir-help" class="form-control" formControlName="bookmarksDirectory" type="text" aria-describedby="change-bookmarks-dir">
<button id="change-bookmarks-dir" class="btn btn-primary" (click)="openDirectoryChooser(settingsForm.get('bookmarksDirectory')?.value, 'bookmarksDirectory')">
Change
</button>
</div>
</div>
</div>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end"> <div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button> <button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">Reset to Default</button>
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button> <button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()">Reset</button>

View File

@ -4,6 +4,8 @@ import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs'; import { take } from 'rxjs';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
import { ServerSettings } from '../_models/server-settings'; import { ServerSettings } from '../_models/server-settings';
import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
selector: 'app-manage-media-settings', selector: 'app-manage-media-settings',
@ -15,23 +17,25 @@ export class ManageMediaSettingsComponent implements OnInit {
serverSettings!: ServerSettings; serverSettings!: ServerSettings;
settingsForm: FormGroup = new FormGroup({}); settingsForm: FormGroup = new FormGroup({});
constructor(private settingsService: SettingsService, private toastr: ToastrService) { } constructor(private settingsService: SettingsService, private toastr: ToastrService, private modalService: NgbModal, ) { }
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, [Validators.required])); 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('convertCoverToWebP', new FormControl(this.serverSettings.convertCoverToWebP, [Validators.required]));
this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required]));
}); });
} }
resetForm() { resetForm() {
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP); this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
this.settingsForm.get('convertCoverToWebP')?.setValue(this.serverSettings.convertCoverToWebP); this.settingsForm.get('convertCoverToWebP')?.setValue(this.serverSettings.convertCoverToWebP);
this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory);
this.settingsForm.markAsPristine(); this.settingsForm.markAsPristine();
} }
async saveSettings() { saveSettings() {
const modelSettings = Object.assign({}, this.serverSettings); const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.convertBookmarkToWebP = this.settingsForm.get('convertBookmarkToWebP')?.value; modelSettings.convertBookmarkToWebP = this.settingsForm.get('convertBookmarkToWebP')?.value;
modelSettings.convertCoverToWebP = this.settingsForm.get('convertCoverToWebP')?.value; modelSettings.convertCoverToWebP = this.settingsForm.get('convertCoverToWebP')?.value;
@ -46,7 +50,7 @@ export class ManageMediaSettingsComponent implements OnInit {
} }
resetToDefaults() { resetToDefaults() {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe(async (settings: ServerSettings) => { this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.resetForm(); this.resetForm();
this.toastr.success('Server settings updated'); this.toastr.success('Server settings updated');
@ -54,4 +58,16 @@ export class ManageMediaSettingsComponent implements OnInit {
console.error('error: ', err); 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();
}
});
}
} }

View File

@ -10,19 +10,6 @@
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text"> <input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
</div> --> </div> -->
<div class="mb-3">
<label for="settings-bookmarksdir" class="form-label">Bookmarks Directory</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="bookmarksDirectoryTooltip" role="button" tabindex="0"></i>
<ng-template #bookmarksDirectoryTooltip>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.</ng-template>
<span class="visually-hidden" id="settings-bookmarksdir-help"><ng-container [ngTemplateOutlet]="bookmarksDirectoryTooltip"></ng-container></span>
<div class="input-group">
<input readonly id="settings-bookmarksdir" aria-describedby="settings-bookmarksdir-help" class="form-control" formControlName="bookmarksDirectory" type="text" aria-describedby="change-bookmarks-dir">
<button id="change-bookmarks-dir" class="btn btn-primary" (click)="openDirectoryChooser(settingsForm.get('bookmarksDirectory')?.value, 'bookmarksDirectory')">
Change
</button>
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="settings-baseurl" class="form-label">Base Url</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i> <label for="settings-baseurl" class="form-label">Base Url</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user.</ng-template> <ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita. Not supported on Docker using non-root user.</ng-template>

View File

@ -1,12 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormGroup, Validators, FormControl } from '@angular/forms'; import { FormGroup, Validators, FormControl } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { TagBadgeCursor } from 'src/app/shared/tag-badge/tag-badge.component'; import { TagBadgeCursor } from 'src/app/shared/tag-badge/tag-badge.component';
import { ServerService } from 'src/app/_services/server.service'; import { ServerService } from 'src/app/_services/server.service';
import { SettingsService } from '../settings.service'; import { SettingsService } from '../settings.service';
import { DirectoryPickerComponent, DirectoryPickerResult } from '../_modals/directory-picker/directory-picker.component';
import { ServerSettings } from '../_models/server-settings'; 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; 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, constructor(private settingsService: SettingsService, private toastr: ToastrService,
private modalService: NgbModal, private serverService: ServerService) { } private serverService: ServerService) { }
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getTaskFrequencies().pipe(take(1)).subscribe(frequencies => { 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.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required])); 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('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required]));
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [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)])); this.settingsForm.addControl('ipAddresses', new FormControl(this.serverSettings.ipAddresses, [Validators.required, Validators.pattern(ValidIpAddress)]));
@ -67,7 +64,6 @@ export class ManageSettingsComponent implements OnInit {
resetForm() { resetForm() {
this.settingsForm.get('cacheDirectory')?.setValue(this.serverSettings.cacheDirectory); 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('scanTask')?.setValue(this.serverSettings.taskScan);
this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup); this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup);
this.settingsForm.get('ipAddresses')?.setValue(this.serverSettings.ipAddresses); 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();
}
});
}
} }

View File

@ -51,7 +51,7 @@
</div> </div>
<div class="float-end"> <div class="float-end">
<button class="btn btn-secondary alt" type="submit">Register</button> <button class="btn btn-secondary alt" type="submit" [disabled]="!registerForm.valid">Register</button>
</div> </div>
</form> </form>
</ng-container> </ng-container>

View File

@ -18,7 +18,7 @@ import { MemberService } from 'src/app/_services/member.service';
export class RegisterComponent { export class RegisterComponent {
registerForm: FormGroup = new FormGroup({ registerForm: FormGroup = new FormGroup({
email: new FormControl('', [Validators.email]), email: new FormControl('', [Validators.required]),
username: new FormControl('', [Validators.required]), username: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]), password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]),
}); });

View File

@ -257,10 +257,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
transformKeyToOpdsUrl(key: string) { transformKeyToOpdsUrl(key: string) {
if (environment.production) { 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() { handleBackgroundColorChange() {

View File

@ -7,7 +7,7 @@
"name": "GPL-3.0", "name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
}, },
"version": "0.7.1.41" "version": "0.7.1.43"
}, },
"servers": [ "servers": [
{ {
@ -2175,6 +2175,7 @@
"tags": [ "tags": [
"Library" "Library"
], ],
"summary": "Return all libraries in the Server",
"responses": { "responses": {
"200": { "200": {
"description": "Success", "description": "Success",
@ -2213,10 +2214,12 @@
"tags": [ "tags": [
"Library" "Library"
], ],
"summary": "For a given library, generate the jump bar information",
"parameters": [ "parameters": [
{ {
"name": "libraryId", "name": "libraryId",
"in": "query", "in": "query",
"description": "",
"schema": { "schema": {
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
@ -2261,7 +2264,9 @@
"tags": [ "tags": [
"Library" "Library"
], ],
"summary": "Grants a user account access to a Library",
"requestBody": { "requestBody": {
"description": "",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@ -2309,10 +2314,12 @@
"tags": [ "tags": [
"Library" "Library"
], ],
"summary": "Scans a given library for file changes.",
"parameters": [ "parameters": [
{ {
"name": "libraryId", "name": "libraryId",
"in": "query", "in": "query",
"description": "",
"schema": { "schema": {
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
@ -2321,6 +2328,31 @@
{ {
"name": "force", "name": "force",
"in": "query", "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": { "schema": {
"type": "boolean", "type": "boolean",
"default": false "default": false
@ -7463,6 +7495,7 @@
"tags": [ "tags": [
"Server" "Server"
], ],
"summary": "Downloads all the log files via a zip",
"responses": { "responses": {
"200": { "200": {
"description": "Success" "description": "Success"
@ -7505,6 +7538,7 @@
"tags": [ "tags": [
"Server" "Server"
], ],
"summary": "Pull the Changelog for Kavita from Github and display",
"responses": { "responses": {
"200": { "200": {
"description": "Success", "description": "Success",
@ -7574,6 +7608,7 @@
"tags": [ "tags": [
"Server" "Server"
], ],
"summary": "Returns a list of reoccurring jobs. Scheduled ad-hoc jobs will not be returned.",
"responses": { "responses": {
"200": { "200": {
"description": "Success", "description": "Success",