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

@ -111,13 +111,21 @@ public class LibraryController : BaseApiController
return Ok(_directoryService.ListDirectory(path));
}
/// <summary>
/// Return all libraries in the Server
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<LibraryDto>>> GetLibraries()
{
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")]
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
{
@ -127,7 +135,11 @@ public class LibraryController : BaseApiController
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")]
[HttpPost("grant-access")]
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.");
}
/// <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")]
[HttpPost("scan")]
public ActionResult Scan(int libraryId, bool force = false)
{
if (libraryId <= 0) return BadRequest("Invalid libraryId");
_taskScheduler.ScanLibrary(libraryId, force);
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")]
[HttpPost("refresh-metadata")]
public ActionResult RefreshMetadata(int libraryId, bool force = true)

View file

@ -156,6 +156,10 @@ public class ServerController : BaseApiController
return Ok();
}
/// <summary>
/// Downloads all the log files via a zip
/// </summary>
/// <returns></returns>
[HttpGet("logs")]
public ActionResult GetLogs()
{
@ -180,6 +184,10 @@ public class ServerController : BaseApiController
return Ok(await _versionUpdaterService.CheckForUpdate());
}
/// <summary>
/// Pull the Changelog for Kavita from Github and display
/// </summary>
/// <returns></returns>
[HttpGet("changelog")]
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
{
@ -198,6 +206,10 @@ public class ServerController : BaseApiController
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")]
public ActionResult<IEnumerable<JobDto>> GetJobs()
{
@ -212,6 +224,7 @@ public class ServerController : BaseApiController
});
return Ok(recurringJobs);
}
}

View file

@ -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, "<script$1></script>");
// 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, @"<title(.*)(/>)", "<title$1></title>", 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, Parser.RegexTimeout);
return content;
}
@ -622,7 +621,7 @@ public class BookService : IBookService
/// <returns></returns>
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;

View file

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

View file

@ -39,12 +39,12 @@ public class ReadingItemService : IReadingItemService
/// <returns></returns>
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
/// <returns></returns>
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);
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()
/// <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))
{
_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)

View file

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

View file

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