diff --git a/API.Tests/Comparers/NaturalSortComparerTest.cs b/API.Tests/Comparers/NaturalSortComparerTest.cs index 60cd287ef..099da0546 100644 --- a/API.Tests/Comparers/NaturalSortComparerTest.cs +++ b/API.Tests/Comparers/NaturalSortComparerTest.cs @@ -7,6 +7,8 @@ namespace API.Tests.Comparers { public class NaturalSortComparerTest { + private readonly NaturalSortComparer _nc = new NaturalSortComparer(); + [Theory] [InlineData( new[] {"x1.jpg", "x10.jpg", "x3.jpg", "x4.jpg", "x11.jpg"}, @@ -20,10 +22,25 @@ namespace API.Tests.Comparers new[] {"[SCX-Scans]_Vandread_v02_Act02.zip", "[SCX-Scans]_Vandread_v02_Act01.zip",}, new[] {"[SCX-Scans]_Vandread_v02_Act01.zip", "[SCX-Scans]_Vandread_v02_Act02.zip",} )] + [InlineData( + new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",}, + new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",} + )] + [InlineData( + new[] {"001.jpg", "10.jpg",}, + new[] {"001.jpg", "10.jpg",} + )] + [InlineData( + new[] {"10/001.jpg", "10.jpg",}, + new[] {"10.jpg", "10/001.jpg",} + )] + [InlineData( + new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, + new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"} + )] public void TestNaturalSortComparer(string[] input, string[] expected) { - NaturalSortComparer nc = new NaturalSortComparer(); - Array.Sort(input, nc); + Array.Sort(input, _nc); var i = 0; foreach (var s in input) @@ -51,10 +68,25 @@ namespace API.Tests.Comparers new[] {"[SCX-Scans]_Vandread_v02_Act02.zip", "[SCX-Scans]_Vandread_v02_Act01.zip","[SCX-Scans]_Vandread_v02_Act07.zip",}, new[] {"[SCX-Scans]_Vandread_v02_Act01.zip", "[SCX-Scans]_Vandread_v02_Act02.zip","[SCX-Scans]_Vandread_v02_Act07.zip",} )] + [InlineData( + new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",}, + new[] {"Frogman v01 001.jpg", "Frogman v01 ch01 p00 Credits.jpg",} + )] + [InlineData( + new[] {"001.jpg", "10.jpg",}, + new[] {"001.jpg", "10.jpg",} + )] + [InlineData( + new[] {"10/001.jpg", "10.jpg",}, + new[] {"10.jpg", "10/001.jpg",} + )] + [InlineData( + new[] {"Batman - Black white vol 1 #04.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr"}, + new[] {"Batman - Black white vol 1 #01.cbr", "Batman - Black white vol 1 #02.cbr", "Batman - Black white vol 1 #03.cbr", "Batman - Black white vol 1 #04.cbr"} + )] public void TestNaturalSortComparerLinq(string[] input, string[] expected) { - NaturalSortComparer nc = new NaturalSortComparer(); - var output = input.OrderBy(c => c, nc); + var output = input.OrderBy(c => c, _nc); var i = 0; foreach (var s in output) diff --git a/API.Tests/ParserTest.cs b/API.Tests/ParserTest.cs index a166d653b..3699c743d 100644 --- a/API.Tests/ParserTest.cs +++ b/API.Tests/ParserTest.cs @@ -366,6 +366,9 @@ namespace API.Tests [InlineData("DearS_v01_cover.jpg", true)] [InlineData("DearS_v01_covers.jpg", false)] [InlineData("!cover.jpg", true)] + [InlineData("cover.jpg", true)] + [InlineData("cover.png", true)] + [InlineData("ch1/cover.png", true)] public void IsCoverImageTest(string inputPath, bool expected) { Assert.Equal(expected, IsCoverImage(inputPath)); diff --git a/API.Tests/Services/ArchiveServiceTests.cs b/API.Tests/Services/ArchiveServiceTests.cs index b8658af0d..8cfa382f2 100644 --- a/API.Tests/Services/ArchiveServiceTests.cs +++ b/API.Tests/Services/ArchiveServiceTests.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.IO.Compression; using API.Archive; @@ -6,6 +7,7 @@ using API.Interfaces.Services; using API.Services; using Microsoft.Extensions.Logging; using NSubstitute; +using NSubstitute.Extensions; using Xunit; using Xunit.Abstractions; @@ -14,7 +16,7 @@ namespace API.Tests.Services public class ArchiveServiceTests { private readonly ITestOutputHelper _testOutputHelper; - private readonly IArchiveService _archiveService; + private readonly ArchiveService _archiveService; private readonly ILogger _logger = Substitute.For>(); public ArchiveServiceTests(ITestOutputHelper testOutputHelper) @@ -113,6 +115,34 @@ namespace API.Tests.Services DirectoryService.ClearAndDeleteDirectory(extractDirectory); } + + + [Theory] + [InlineData(new [] {"folder.jpg"}, "folder.jpg")] + [InlineData(new [] {"vol1/"}, "")] + [InlineData(new [] {"folder.jpg", "vol1/folder.jpg"}, "folder.jpg")] + [InlineData(new [] {"cover.jpg", "vol1/folder.jpg"}, "cover.jpg")] + [InlineData(new [] {"__MACOSX/cover.jpg", "vol1/page 01.jpg"}, "")] + [InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "folder.jpg")] + public void FindFolderEntry(string[] files, string expected) + { + var foundFile = _archiveService.FindFolderEntry(files); + Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile); + } + + [Theory] + [InlineData(new [] {"folder.jpg"}, "folder.jpg")] + [InlineData(new [] {"vol1/"}, "")] + [InlineData(new [] {"folder.jpg", "vol1/folder.jpg"}, "folder.jpg")] + [InlineData(new [] {"cover.jpg", "vol1/folder.jpg"}, "cover.jpg")] + [InlineData(new [] {"page 2.jpg", "page 10.jpg"}, "page 2.jpg")] + [InlineData(new [] {"__MACOSX/cover.jpg", "vol1/page 01.jpg"}, "vol1/page 01.jpg")] + [InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg")] + public void FindFirstEntry(string[] files, string expected) + { + var foundFile = _archiveService.FirstFileEntry(files); + Assert.Equal(expected, string.IsNullOrEmpty(foundFile) ? "" : foundFile); + } @@ -122,12 +152,37 @@ namespace API.Tests.Services [InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")] //[InlineData("png.zip", "png.PNG")] [InlineData("macos_native.zip", "macos_native.jpg")] - public void GetCoverImageTest(string inputFile, string expectedOutputFile) + [InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.jpg")] + [InlineData("sorting.zip", "sorting.expected.jpg")] + public void GetCoverImage_Default_Test(string inputFile, string expectedOutputFile) { + var archiveService = Substitute.For(_logger); var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages"); var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile)); + archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.Default); Stopwatch sw = Stopwatch.StartNew(); - Assert.Equal(expectedBytes, _archiveService.GetCoverImage(Path.Join(testDirectory, inputFile))); + Assert.Equal(expectedBytes, archiveService.GetCoverImage(Path.Join(testDirectory, inputFile))); + _testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms"); + } + + + [Theory] + [InlineData("v10.cbz", "v10.expected.jpg")] + [InlineData("v10 - with folder.cbz", "v10 - with folder.expected.jpg")] + [InlineData("v10 - nested folder.cbz", "v10 - nested folder.expected.jpg")] + //[InlineData("png.zip", "png.PNG")] + [InlineData("macos_native.zip", "macos_native.jpg")] + [InlineData("v10 - duplicate covers.cbz", "v10 - duplicate covers.expected.jpg")] + [InlineData("sorting.zip", "sorting.expected.jpg")] + public void GetCoverImage_SharpCompress_Test(string inputFile, string expectedOutputFile) + { + var archiveService = Substitute.For(_logger); + var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/CoverImages"); + var expectedBytes = File.ReadAllBytes(Path.Join(testDirectory, expectedOutputFile)); + + archiveService.Configure().CanOpen(Path.Join(testDirectory, inputFile)).Returns(ArchiveLibrary.SharpCompress); + Stopwatch sw = Stopwatch.StartNew(); + Assert.Equal(expectedBytes, archiveService.GetCoverImage(Path.Join(testDirectory, inputFile))); _testOutputHelper.WriteLine($"Processed in {sw.ElapsedMilliseconds} ms"); } diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.jpg b/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.jpg new file mode 100644 index 000000000..bd9d441cd Binary files /dev/null and b/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.expected.jpg differ diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip b/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip new file mode 100644 index 000000000..88e6fe03d Binary files /dev/null and b/API.Tests/Services/Test Data/ArchiveService/CoverImages/sorting.zip differ diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz b/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz new file mode 100644 index 000000000..20cc070f9 Binary files /dev/null and b/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.cbz differ diff --git a/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.jpg b/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.jpg new file mode 100644 index 000000000..51fd89ca0 Binary files /dev/null and b/API.Tests/Services/Test Data/ArchiveService/CoverImages/v10 - duplicate covers.expected.jpg differ diff --git a/API/Comparators/NaturalSortComparer.cs b/API/Comparators/NaturalSortComparer.cs index 961ab40ff..c3beebef1 100644 --- a/API/Comparators/NaturalSortComparer.cs +++ b/API/Comparators/NaturalSortComparer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; using static System.GC; +using static System.String; namespace API.Comparators { @@ -17,20 +18,18 @@ namespace API.Comparators int IComparer.Compare(string x, string y) { - if (x == y) - return 0; + if (x == y) return 0; - string[] x1, y1; - - if (!_table.TryGetValue(x, out x1)) + if (!_table.TryGetValue(x, out var x1)) { - x1 = Regex.Split(x.Replace(" ", ""), "([0-9]+)"); + // .Replace(" ", Empty) + x1 = Regex.Split(x, "([0-9]+)"); _table.Add(x, x1); } - if (!_table.TryGetValue(y ?? string.Empty, out y1)) + if (!_table.TryGetValue(y, out var y1)) { - y1 = Regex.Split(y?.Replace(" ", ""), "([0-9]+)"); + y1 = Regex.Split(y, "([0-9]+)"); _table.Add(y, y1); } @@ -61,12 +60,11 @@ namespace API.Comparators private static int PartCompare(string left, string right) { - int x, y; - if (!int.TryParse(left, out x)) - return left.CompareTo(right); + if (!int.TryParse(left, out var x)) + return Compare(left, right, StringComparison.Ordinal); - if (!int.TryParse(right, out y)) - return left.CompareTo(right); + if (!int.TryParse(right, out var y)) + return Compare(left, right, StringComparison.Ordinal); return x.CompareTo(y); } diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index d482193ce..9b0e23e56 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -92,6 +92,7 @@ namespace API.Data /// public async Task GetFullLibraryForIdAsync(int libraryId) { + return await _context.Library .Where(x => x.Id == libraryId) .Include(f => f.Folders) diff --git a/API/Parser/Parser.cs b/API/Parser/Parser.cs index af6124309..4e5521dfc 100644 --- a/API/Parser/Parser.cs +++ b/API/Parser/Parser.cs @@ -15,7 +15,7 @@ namespace API.Parser private static readonly Regex ImageRegex = new Regex(ImageFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex ArchiveFileRegex = new Regex(ArchiveFileExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex XmlRegex = new Regex(XmlRegexExtensions, RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex FolderRegex = new Regex(@"(? public static bool IsCoverImage(string name) { - return IsImage(name, true) && (FolderRegex.IsMatch(name)); + return IsImage(name, true) && (CoverImageRegex.IsMatch(name)); } public static bool HasBlacklistedFolderInPath(string path) diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs index fba12280b..2e9f717f1 100644 --- a/API/Services/ArchiveService.cs +++ b/API/Services/ArchiveService.cs @@ -39,7 +39,7 @@ namespace API.Services /// /// /// - public ArchiveLibrary CanOpen(string archivePath) + public virtual ArchiveLibrary CanOpen(string archivePath) { if (!File.Exists(archivePath) || !Parser.Parser.IsArchive(archivePath)) return ArchiveLibrary.NotSupported; @@ -103,11 +103,42 @@ namespace API.Services return 0; } } + + /// + /// Finds the first instance of a folder entry and returns it + /// + /// + /// Entry name of match, null if no match + public string FindFolderEntry(IEnumerable entryFullNames) + { + var result = entryFullNames + .FirstOrDefault(x => !Path.EndsInDirectorySeparator(x) && !Parser.Parser.HasBlacklistedFolderInPath(x) + && Parser.Parser.IsCoverImage(x)); + + return string.IsNullOrEmpty(result) ? null : result; + } + + /// + /// Returns first entry that is an image and is not in a blacklisted folder path. Uses for ordering files + /// + /// + /// Entry name of match, null if no match + public string FirstFileEntry(IEnumerable entryFullNames) + { + var result = entryFullNames.OrderBy(Path.GetFileName, _comparer) + .FirstOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x) + && Parser.Parser.IsImage(x)); + + return string.IsNullOrEmpty(result) ? null : result; + } + /// /// Generates byte array of cover image. - /// Given a path to a compressed file (zip, rar, cbz, cbr, etc), will ensure the first image is returned unless - /// a folder.extension exists in the root directory of the compressed file. + /// Given a path to a compressed file , will ensure the first image (respects directory structure) is returned unless + /// a folder/cover.(image extension) exists in the the compressed file (if duplicate, the first is chosen) + /// + /// This skips over any __MACOSX folder/file iteration. /// /// /// Create a smaller variant of file extracted from archive. Archive images are usually 1MB each. @@ -124,28 +155,28 @@ namespace API.Services { _logger.LogDebug("Using default compression handling"); using var archive = ZipFile.OpenRead(archivePath); - // NOTE: We can probably reduce our iteration by performing 1 filter on MACOSX then do our folder check and image chack. - var folder = archive.Entries.SingleOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x.FullName) - && Parser.Parser.IsImage(x.FullName) - && Parser.Parser.IsCoverImage(x.FullName)); - var entries = archive.Entries.Where(x => Path.HasExtension(x.FullName) - && !Parser.Parser.HasBlacklistedFolderInPath(x.FullName) - && Parser.Parser.IsImage(x.FullName)) - .OrderBy(x => x.FullName, _comparer).ToList(); - var entry = folder ?? entries[0]; + var entryNames = archive.Entries.Select(e => e.FullName).ToArray(); - return createThumbnail ? CreateThumbnail(entry) : ConvertEntryToByteArray(entry); + var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames); + var entry = archive.Entries.Single(e => e.FullName == entryName); + using var stream = entry.Open(); + + return createThumbnail ? CreateThumbnail(entry.FullName, stream) : ConvertEntryToByteArray(entry); } case ArchiveLibrary.SharpCompress: { _logger.LogDebug("Using SharpCompress compression handling"); using var archive = ArchiveFactory.Open(archivePath); - var entries = archive.Entries - .Where(entry => !entry.IsDirectory - && !Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty) - && Parser.Parser.IsImage(entry.Key)) - .OrderBy(x => x.Key, _comparer); - return FindCoverImage(entries, createThumbnail); + var entryNames = archive.Entries.Where(entry => !entry.IsDirectory).Select(e => e.Key).ToList(); + + var entryName = FindFolderEntry(entryNames) ?? FirstFileEntry(entryNames); + var entry = archive.Entries.Single(e => e.Key == entryName); + + using var ms = _streamManager.GetStream(); + entry.WriteTo(ms); + ms.Position = 0; + + return createThumbnail ? CreateThumbnail(entry.Key, ms, Path.GetExtension(entry.Key)) : ms.ToArray(); } case ArchiveLibrary.NotSupported: _logger.LogError("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); @@ -163,35 +194,6 @@ namespace API.Services return Array.Empty(); } - private byte[] FindCoverImage(IEnumerable entries, bool createThumbnail) - { - var images = entries.ToList(); - foreach (var entry in images) - { - if (Path.GetFileNameWithoutExtension(entry.Key).ToLower() == "folder") - { - using var ms = _streamManager.GetStream(); - entry.WriteTo(ms); - ms.Position = 0; - var data = ms.ToArray(); - return createThumbnail ? CreateThumbnail(data, Path.GetExtension(entry.Key)) : data; - } - } - - if (images.Any()) - { - var entry = images.OrderBy(e => e.Key).FirstOrDefault(); - if (entry == null) return Array.Empty(); - using var ms = _streamManager.GetStream(); - entry.WriteTo(ms); - ms.Position = 0; - var data = ms.ToArray(); - return createThumbnail ? CreateThumbnail(data, Path.GetExtension(entry.Key)) : data; - } - - return Array.Empty(); - } - private static byte[] ConvertEntryToByteArray(ZipArchiveEntry entry) { using var stream = entry.Open(); @@ -213,28 +215,8 @@ namespace API.Services !Path.HasExtension(archive.Entries.ElementAt(0).FullName) || archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.Parser.HasBlacklistedFolderInPath(e.FullName)); } - - private byte[] CreateThumbnail(byte[] entry, string formatExtension = ".jpg") - { - if (!formatExtension.StartsWith(".")) - { - formatExtension = "." + formatExtension; - } - - try - { - using var thumbnail = Image.ThumbnailBuffer(entry, ThumbnailWidth); - return thumbnail.WriteToBuffer(formatExtension); - } - catch (Exception ex) - { - _logger.LogError(ex, "[CreateThumbnail] There was a critical error and prevented thumbnail generation. Defaulting to no cover image. Format Extension {Extension}", formatExtension); - } - - return Array.Empty(); - } - private byte[] CreateThumbnail(ZipArchiveEntry entry, string formatExtension = ".jpg") + private byte[] CreateThumbnail(string entryName, Stream stream, string formatExtension = ".jpg") { if (!formatExtension.StartsWith(".")) { @@ -242,13 +224,12 @@ namespace API.Services } try { - using var stream = entry.Open(); using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth); return thumbnail.WriteToBuffer(formatExtension); } catch (Exception ex) { - _logger.LogError(ex, "There was a critical error and prevented thumbnail generation on {EntryName}. Defaulting to no cover image", entry.FullName); + _logger.LogError(ex, "There was a critical error and prevented thumbnail generation on {EntryName}. Defaulting to no cover image", entryName); } return Array.Empty(); diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs index 1a7c7fe3e..f86cb3595 100644 --- a/API/Services/MetadataService.cs +++ b/API/Services/MetadataService.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using API.Comparators; using API.Entities; using API.Extensions; using API.Interfaces; @@ -41,19 +40,19 @@ namespace API.Services } } - + public void UpdateMetadata(Volume volume, bool forceUpdate) { if (volume != null && ShouldFindCoverImage(volume.CoverImage, forceUpdate)) { // TODO: Replace this with ChapterSortComparator volume.Chapters ??= new List(); - var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number)).FirstOrDefault(); + var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number)).FirstOrDefault(); - var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault(); // Skip calculating Cover Image (I/O) if the chapter already has it set if (firstChapter == null || ShouldFindCoverImage(firstChapter.CoverImage)) { + var firstFile = firstChapter?.Files.OrderBy(x => x.Chapter).FirstOrDefault(); if (firstFile != null && !new FileInfo(firstFile.FilePath).IsLastWriteLessThan(firstFile.LastModified)) { volume.CoverImage = _archiveService.GetCoverImage(firstFile.FilePath, true); @@ -112,6 +111,7 @@ namespace API.Services var sw = Stopwatch.StartNew(); var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).Result; + // TODO: See if we can break this up into multiple threads that process 20 series at a time then save so we can reduce amount of memory used _logger.LogInformation("Beginning metadata refresh of {LibraryName}", library.Name); foreach (var series in library.Series) { diff --git a/API/Services/TaskScheduler.cs b/API/Services/TaskScheduler.cs index b4c0e8cdf..23936db8a 100644 --- a/API/Services/TaskScheduler.cs +++ b/API/Services/TaskScheduler.cs @@ -40,7 +40,7 @@ namespace API.Services { _logger.LogInformation("Scheduling reoccurring tasks"); - string setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).Result.Value; + string setting = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskScan)).GetAwaiter().GetResult().Value; if (setting != null) { _logger.LogDebug("Scheduling Scan Library Task for {Setting}", setting);