Scan Chunking (#604)
* Some performance refactoring around getting Library and avoid a byte[] copy for getting cover images for epubs. * Initial commit. Rewrote the main series scan loop to use chunks of data at a time. Not fully shaken out. * Hooked in the ability for the UI to react to series being added or removed from the DB. * Cleaned up the messaging in the scan loop to be more clear. * Metadata scan and scan work as expected and populate data to the UI. There is a slow down in speed for overall operation. Scan series and refresh series metadata does not work fully. * Fixed a bug where MangaFiles were not having LastModified Updated correctly, meaning they were opening archives every scan. * Modified the code to be more realistic to the underlying file * Updated ScanService to properly handle deleted files and not result in a higher-level scan. * Shuffled around volume related repo apis to the volume repo rather than being in series. * Rewrote scan series to be much cleaner and more concise on the flow. Fixed an issue in UpdateVolumes such that the debug code to log out removed volumes could throw an exception and actually break updating volumes. * Refactored the code to set MangaFile last modified timestamp into the MangaFile entity. * Added Series Name to ScanSeries event * Added additional checks in ScanSeries to ensure we never go outside the library folder. Added extra debug messages for when a metadata refresh doesn't actually make changes and for when we regen cover images. * More logging statements saying where they originate from. Fixed a critical bug which caused only 1 chunk to ever be processed. * Fixed a concurrency issue with natural sorter which could cause issues in ArchiveService.cs. * Log cleanups * Fixed an issue with logging out total time of a scan. * Only show added toastrs for admins. When kicking off a refresh metadata for series, make sure we regenerate all cover images. * Code smells on benchmark despite it being ignored
This commit is contained in:
parent
2b50fd6380
commit
56cf7be799
42 changed files with 1503 additions and 403 deletions
|
@ -13,10 +13,8 @@ using API.Interfaces.Services;
|
|||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IO;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using Image = NetVips.Image;
|
||||
|
||||
namespace API.Services
|
||||
{
|
||||
|
@ -28,14 +26,12 @@ namespace API.Services
|
|||
{
|
||||
private readonly ILogger<ArchiveService> _logger;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly NaturalSortComparer _comparer;
|
||||
private const string ComicInfoFilename = "comicinfo";
|
||||
|
||||
public ArchiveService(ILogger<ArchiveService> logger, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_directoryService = directoryService;
|
||||
_comparer = new NaturalSortComparer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -81,13 +77,11 @@ namespace API.Services
|
|||
{
|
||||
case ArchiveLibrary.Default:
|
||||
{
|
||||
_logger.LogDebug("Using default compression handling");
|
||||
using ZipArchive archive = ZipFile.OpenRead(archivePath);
|
||||
using var archive = ZipFile.OpenRead(archivePath);
|
||||
return archive.Entries.Count(e => !Parser.Parser.HasBlacklistedFolderInPath(e.FullName) && Parser.Parser.IsImage(e.FullName));
|
||||
}
|
||||
case ArchiveLibrary.SharpCompress:
|
||||
{
|
||||
_logger.LogDebug("Using SharpCompress compression handling");
|
||||
using var archive = ArchiveFactory.Open(archivePath);
|
||||
return archive.Entries.Count(entry => !entry.IsDirectory &&
|
||||
!Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty)
|
||||
|
@ -130,7 +124,7 @@ namespace API.Services
|
|||
/// <returns>Entry name of match, null if no match</returns>
|
||||
public string FirstFileEntry(IEnumerable<string> entryFullNames)
|
||||
{
|
||||
var result = entryFullNames.OrderBy(Path.GetFileName, _comparer)
|
||||
var result = entryFullNames.OrderBy(Path.GetFileName, new NaturalSortComparer())
|
||||
.FirstOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x)
|
||||
&& Parser.Parser.IsImage(x)
|
||||
&& !x.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith));
|
||||
|
@ -160,7 +154,6 @@ namespace API.Services
|
|||
{
|
||||
case ArchiveLibrary.Default:
|
||||
{
|
||||
_logger.LogDebug("Using default compression handling");
|
||||
using var archive = ZipFile.OpenRead(archivePath);
|
||||
var entryNames = archive.Entries.Select(e => e.FullName).ToArray();
|
||||
|
||||
|
@ -172,7 +165,6 @@ namespace API.Services
|
|||
}
|
||||
case ArchiveLibrary.SharpCompress:
|
||||
{
|
||||
_logger.LogDebug("Using SharpCompress compression handling");
|
||||
using var archive = ArchiveFactory.Open(archivePath);
|
||||
var entryNames = archive.Entries.Where(archiveEntry => !archiveEntry.IsDirectory).Select(e => e.Key).ToList();
|
||||
|
||||
|
@ -316,7 +308,6 @@ namespace API.Services
|
|||
{
|
||||
case ArchiveLibrary.Default:
|
||||
{
|
||||
_logger.LogTrace("Using default compression handling");
|
||||
using var archive = ZipFile.OpenRead(archivePath);
|
||||
var entry = archive.Entries.SingleOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x.FullName)
|
||||
&& Path.GetFileNameWithoutExtension(x.Name)?.ToLower() == ComicInfoFilename
|
||||
|
@ -332,7 +323,6 @@ namespace API.Services
|
|||
}
|
||||
case ArchiveLibrary.SharpCompress:
|
||||
{
|
||||
_logger.LogTrace("Using SharpCompress compression handling");
|
||||
using var archive = ArchiveFactory.Open(archivePath);
|
||||
info = FindComicInfoXml(archive.Entries.Where(entry => !entry.IsDirectory
|
||||
&& !Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty)
|
||||
|
@ -410,14 +400,12 @@ namespace API.Services
|
|||
{
|
||||
case ArchiveLibrary.Default:
|
||||
{
|
||||
_logger.LogDebug("Using default compression handling");
|
||||
using var archive = ZipFile.OpenRead(archivePath);
|
||||
ExtractArchiveEntries(archive, extractPath);
|
||||
break;
|
||||
}
|
||||
case ArchiveLibrary.SharpCompress:
|
||||
{
|
||||
_logger.LogDebug("Using SharpCompress compression handling");
|
||||
using var archive = ArchiveFactory.Open(archivePath);
|
||||
ExtractArchiveEntities(archive.Entries.Where(entry => !entry.IsDirectory
|
||||
&& !Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty)
|
||||
|
|
|
@ -393,7 +393,7 @@ namespace API.Services
|
|||
/// <returns></returns>
|
||||
public string GetCoverImage(string fileFilePath, string fileName)
|
||||
{
|
||||
if (!IsValidFile(fileFilePath)) return String.Empty;
|
||||
if (!IsValidFile(fileFilePath)) return string.Empty;
|
||||
|
||||
if (Parser.Parser.IsPdf(fileFilePath))
|
||||
{
|
||||
|
@ -411,8 +411,8 @@ namespace API.Services
|
|||
?? epubBook.Content.Images.Values.FirstOrDefault();
|
||||
|
||||
if (coverImageContent == null) return string.Empty;
|
||||
using var stream = coverImageContent.GetContentStream();
|
||||
|
||||
using var stream = StreamManager.GetStream("BookService.GetCoverImage", coverImageContent.ReadContent());
|
||||
return ImageService.WriteCoverThumbnail(stream, fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
@ -47,6 +47,8 @@ namespace API.Services
|
|||
var firstImage = _directoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions)
|
||||
.OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault();
|
||||
|
||||
|
||||
|
||||
return firstImage;
|
||||
}
|
||||
|
||||
|
@ -73,7 +75,7 @@ namespace API.Services
|
|||
{
|
||||
using var thumbnail = Image.Thumbnail(path, ThumbnailWidth);
|
||||
var filename = fileName + ".png";
|
||||
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png"));
|
||||
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, filename));
|
||||
return filename;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
@ -5,9 +5,11 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using API.SignalR;
|
||||
|
@ -74,7 +76,7 @@ namespace API.Services
|
|||
|
||||
private string GetCoverImage(MangaFile file, int volumeId, int chapterId)
|
||||
{
|
||||
file.LastModified = DateTime.Now;
|
||||
file.UpdateLastModified();
|
||||
switch (file.Format)
|
||||
{
|
||||
case MangaFormat.Pdf:
|
||||
|
@ -102,6 +104,7 @@ namespace API.Services
|
|||
|
||||
if (ShouldUpdateCoverImage(chapter.CoverImage, firstFile, forceUpdate, chapter.CoverImageLocked))
|
||||
{
|
||||
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile?.FilePath);
|
||||
chapter.CoverImage = GetCoverImage(firstFile, chapter.VolumeId, chapter.Id);
|
||||
return true;
|
||||
}
|
||||
|
@ -117,8 +120,7 @@ namespace API.Services
|
|||
public bool UpdateMetadata(Volume volume, bool forceUpdate)
|
||||
{
|
||||
// We need to check if Volume coverImage matches first chapters if forceUpdate is false
|
||||
if (volume == null || !ShouldUpdateCoverImage(volume.CoverImage, null, forceUpdate
|
||||
, false)) return false;
|
||||
if (volume == null || !ShouldUpdateCoverImage(volume.CoverImage, null, forceUpdate)) return false;
|
||||
|
||||
volume.Chapters ??= new List<Chapter>();
|
||||
var firstChapter = volume.Chapters.OrderBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting).FirstOrDefault();
|
||||
|
@ -183,11 +185,9 @@ namespace API.Services
|
|||
if (!string.IsNullOrEmpty(series.Summary))
|
||||
{
|
||||
series.Summary = summary;
|
||||
firstFile.LastModified = DateTime.Now;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
firstFile.LastModified = DateTime.Now; // NOTE: Should I put this here as well since it might not have actually been parsed?
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -200,34 +200,57 @@ namespace API.Services
|
|||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
|
||||
_logger.LogInformation("[MetadataService] Beginning metadata refresh of {LibraryName}", library.Name);
|
||||
|
||||
// PERF: 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)
|
||||
var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var totalTime = 0L;
|
||||
|
||||
for (var chunk = 0; chunk <= chunkInfo.TotalChunks; chunk++)
|
||||
{
|
||||
var volumeUpdated = false;
|
||||
foreach (var volume in series.Volumes)
|
||||
{
|
||||
var chapterUpdated = false;
|
||||
foreach (var chapter in volume.Chapters)
|
||||
totalTime += stopwatch.ElapsedMilliseconds;
|
||||
stopwatch.Restart();
|
||||
_logger.LogDebug($"[MetadataService] Processing chunk {chunk} / {chunkInfo.TotalChunks} with size {chunkInfo.ChunkSize} Series ({chunk * chunkInfo.ChunkSize} - {(chunk + 1) * chunkInfo.ChunkSize}");
|
||||
var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id,
|
||||
new UserParams()
|
||||
{
|
||||
chapterUpdated = UpdateMetadata(chapter, forceUpdate);
|
||||
PageNumber = chunk,
|
||||
PageSize = chunkInfo.ChunkSize
|
||||
});
|
||||
|
||||
Parallel.ForEach(nonLibrarySeries, series =>
|
||||
{
|
||||
_logger.LogDebug("[MetadataService] Processing series {SeriesName}", series.OriginalName);
|
||||
var volumeUpdated = false;
|
||||
foreach (var volume in series.Volumes)
|
||||
{
|
||||
var chapterUpdated = false;
|
||||
foreach (var chapter in volume.Chapters)
|
||||
{
|
||||
chapterUpdated = UpdateMetadata(chapter, forceUpdate);
|
||||
}
|
||||
|
||||
volumeUpdated = UpdateMetadata(volume, chapterUpdated || forceUpdate);
|
||||
}
|
||||
|
||||
volumeUpdated = UpdateMetadata(volume, chapterUpdated || forceUpdate);
|
||||
UpdateMetadata(series, volumeUpdated || forceUpdate);
|
||||
});
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[MetadataService] Processed {SeriesStart} - {SeriesEnd} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize, stopwatch.ElapsedMilliseconds, library.Name);
|
||||
|
||||
foreach (var series in nonLibrarySeries)
|
||||
{
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.RefreshMetadata, MessageFactory.RefreshMetadataEvent(library.Id, series.Id));
|
||||
}
|
||||
}
|
||||
|
||||
UpdateMetadata(series, volumeUpdated || forceUpdate);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
}
|
||||
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds);
|
||||
}
|
||||
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesNumber} series in library {LibraryName} in {ElapsedMilliseconds} milliseconds total", chunkInfo.TotalSize, library.Name, totalTime);
|
||||
}
|
||||
|
||||
|
||||
|
@ -239,15 +262,13 @@ namespace API.Services
|
|||
public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
|
||||
|
||||
var series = library.Series.SingleOrDefault(s => s.Id == seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId);
|
||||
if (series == null)
|
||||
{
|
||||
_logger.LogError("Series {SeriesId} was not found on Library {LibraryName}", seriesId, libraryId);
|
||||
_logger.LogError("[MetadataService] Series {SeriesId} was not found on Library {LibraryId}", seriesId, libraryId);
|
||||
return;
|
||||
}
|
||||
_logger.LogInformation("Beginning metadata refresh of {SeriesName}", series.Name);
|
||||
_logger.LogInformation("[MetadataService] Beginning metadata refresh of {SeriesName}", series.Name);
|
||||
var volumeUpdated = false;
|
||||
foreach (var volume in series.Volumes)
|
||||
{
|
||||
|
@ -261,14 +282,14 @@ namespace API.Services
|
|||
}
|
||||
|
||||
UpdateMetadata(series, volumeUpdated || forceUpdate);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
|
||||
|
||||
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation("Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.RefreshMetadataEvent(libraryId, seriesId));
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.RefreshMetadata, MessageFactory.RefreshMetadataEvent(series.LibraryId, series.Id));
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,7 +119,7 @@ namespace API.Services
|
|||
public void ScanLibrary(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate));
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId));
|
||||
// When we do a scan, force cache to re-unpack in case page numbers change
|
||||
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ namespace API.Services
|
|||
BackgroundJob.Enqueue(() => DirectoryService.ClearDirectory(tempDirectory));
|
||||
}
|
||||
|
||||
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = true)
|
||||
{
|
||||
_logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId);
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
|
||||
|
@ -150,7 +150,7 @@ namespace API.Services
|
|||
public void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
{
|
||||
_logger.LogInformation("Enqueuing series scan for: {SeriesId}", seriesId);
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanSeries(libraryId, seriesId, forceUpdate, CancellationToken.None));
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanSeries(libraryId, seriesId, CancellationToken.None));
|
||||
}
|
||||
|
||||
public void BackupDatabase()
|
||||
|
|
|
@ -7,9 +7,11 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Interfaces;
|
||||
using API.Interfaces.Services;
|
||||
using API.Parser;
|
||||
|
@ -46,81 +48,103 @@ namespace API.Services.Tasks
|
|||
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 360)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanSeries(int libraryId, int seriesId, bool forceUpdate, CancellationToken token)
|
||||
public async Task ScanSeries(int libraryId, int seriesId, CancellationToken token)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||
var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId, seriesId);
|
||||
var dirs = DirectoryService.FindHighestDirectoriesFromFiles(library.Folders.Select(f => f.Path), files.Select(f => f.FilePath).ToList());
|
||||
var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{ seriesId });
|
||||
var chapterIds = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId});
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders);
|
||||
var folderPaths = library.Folders.Select(f => f.Path).ToList();
|
||||
var dirs = DirectoryService.FindHighestDirectoriesFromFiles(folderPaths, files.Select(f => f.FilePath).ToList());
|
||||
|
||||
_logger.LogInformation("Beginning file scan on {SeriesName}", series.Name);
|
||||
var scanner = new ParseScannedFiles(_bookService, _logger);
|
||||
var parsedSeries = scanner.ScanLibrariesForSeries(library.Type, dirs.Keys, out var totalFiles, out var scanElapsedTime);
|
||||
|
||||
// If a root level folder scan occurs, then multiple series gets passed in and thus we get a unique constraint issue
|
||||
// Hence we clear out anything but what we selected for
|
||||
var firstSeries = library.Series.FirstOrDefault();
|
||||
// Remove any parsedSeries keys that don't belong to our series. This can occur when users store 2 series in the same folder
|
||||
RemoveParsedInfosNotForSeries(parsedSeries, series);
|
||||
|
||||
// If nothing was found, first validate any of the files still exist. If they don't then we have a deletion and can skip the rest of the logic flow
|
||||
if (parsedSeries.Count == 0)
|
||||
{
|
||||
var anyFilesExist =
|
||||
(await _unitOfWork.SeriesRepository.GetFilesForSeries(series.Id)).Any(m => File.Exists(m.FilePath));
|
||||
|
||||
if (!anyFilesExist)
|
||||
{
|
||||
_unitOfWork.SeriesRepository.Remove(series);
|
||||
await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to do an additional check for an edge case: If the scan ran and the files do not match the existing Series name, then it is very likely,
|
||||
// the files have crap naming and if we don't correct, the series will get deleted due to the parser not being able to fallback onto folder parsing as the root
|
||||
// is the series folder.
|
||||
var existingFolder = dirs.Keys.FirstOrDefault(key => key.Contains(series.OriginalName));
|
||||
if (dirs.Keys.Count == 1 && !string.IsNullOrEmpty(existingFolder))
|
||||
{
|
||||
dirs = new Dictionary<string, string>();
|
||||
var path = Directory.GetParent(existingFolder)?.FullName;
|
||||
if (!folderPaths.Contains(path) || !folderPaths.Any(p => p.Contains(path ?? string.Empty)))
|
||||
{
|
||||
_logger.LogInformation("[ScanService] Aborted: {SeriesName} has bad naming convention and sits at root of library. Cannot scan series without deletion occuring. Correct file names to have Series Name within it or perform Scan Library", series.OriginalName);
|
||||
return;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
dirs[path] = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("{SeriesName} has bad naming convention, forcing rescan at a higher directory.", series.OriginalName);
|
||||
scanner = new ParseScannedFiles(_bookService, _logger);
|
||||
parsedSeries = scanner.ScanLibrariesForSeries(library.Type, dirs.Keys, out var totalFiles2, out var scanElapsedTime2);
|
||||
totalFiles += totalFiles2;
|
||||
scanElapsedTime += scanElapsedTime2;
|
||||
RemoveParsedInfosNotForSeries(parsedSeries, series);
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, parsedSeries will have at least one key and we can perform the update. If it still doesn't, just return and don't do anything
|
||||
if (parsedSeries.Count == 0) return;
|
||||
|
||||
UpdateSeries(series, parsedSeries);
|
||||
await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds, token);
|
||||
}
|
||||
|
||||
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
||||
{
|
||||
var keys = parsedSeries.Keys;
|
||||
foreach (var key in keys.Where(key => !firstSeries.NameInParserInfo(parsedSeries[key].FirstOrDefault()) || firstSeries?.Format != key.Format))
|
||||
foreach (var key in keys.Where(key =>
|
||||
!series.NameInParserInfo(parsedSeries[key].FirstOrDefault()) || series.Format != key.Format))
|
||||
{
|
||||
parsedSeries.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedSeries.Count == 0)
|
||||
private async Task CommitAndSend(int libraryId, int seriesId, int totalFiles,
|
||||
Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series, int[] chapterIds, CancellationToken token)
|
||||
{
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
// We need to do an additional check for an edge case: If the scan ran and the files do not match the existing Series name, then it is very likely,
|
||||
// the files have crap naming and if we don't correct, the series will get deleted due to the parser not being able to fallback onto folder parsing as the root
|
||||
// is the series folder.
|
||||
var existingFolder = dirs.Keys.FirstOrDefault(key => key.Contains(series.OriginalName));
|
||||
if (dirs.Keys.Count == 1 && !string.IsNullOrEmpty(existingFolder))
|
||||
{
|
||||
dirs = new Dictionary<string, string>();
|
||||
var path = Path.GetPathRoot(existingFolder);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
dirs[path] = string.Empty;
|
||||
}
|
||||
}
|
||||
_logger.LogDebug("{SeriesName} has bad naming convention, forcing rescan at a higher directory.", series.OriginalName);
|
||||
scanner = new ParseScannedFiles(_bookService, _logger);
|
||||
parsedSeries = scanner.ScanLibrariesForSeries(library.Type, dirs.Keys, out var totalFiles2, out var scanElapsedTime2);
|
||||
totalFiles += totalFiles2;
|
||||
scanElapsedTime += scanElapsedTime2;
|
||||
_logger.LogInformation(
|
||||
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
||||
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
||||
|
||||
// If a root level folder scan occurs, then multiple series gets passed in and thus we get a unique constraint issue
|
||||
// Hence we clear out anything but what we selected for
|
||||
firstSeries = library.Series.FirstOrDefault();
|
||||
keys = parsedSeries.Keys;
|
||||
foreach (var key in keys.Where(key => !firstSeries.NameInParserInfo(parsedSeries[key].FirstOrDefault()) || firstSeries?.Format != key.Format))
|
||||
{
|
||||
parsedSeries.Remove(key);
|
||||
}
|
||||
await CleanupDbEntities();
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, false));
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||
// Tell UI that this series is done
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId, series.Name),
|
||||
cancellationToken: token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCritical(
|
||||
"There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
var sw = new Stopwatch();
|
||||
UpdateLibrary(library, parsedSeries);
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
||||
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
||||
|
||||
await CleanupDbEntities();
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||
// Tell UI that this series is done
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId), cancellationToken: token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCritical(
|
||||
"There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
||||
await _unitOfWork.RollbackAsync();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -132,7 +156,7 @@ namespace API.Services.Tasks
|
|||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var lib in libraries)
|
||||
{
|
||||
await ScanLibrary(lib.Id, false);
|
||||
await ScanLibrary(lib.Id);
|
||||
}
|
||||
_logger.LogInformation("Scan of All Libraries Finished");
|
||||
}
|
||||
|
@ -144,24 +168,23 @@ namespace API.Services.Tasks
|
|||
/// ie) all entities will be rechecked for new cover images and comicInfo.xml changes
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
[DisableConcurrentExecution(360)]
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibrary(int libraryId, bool forceUpdate)
|
||||
public async Task ScanLibrary(int libraryId)
|
||||
{
|
||||
Library library;
|
||||
try
|
||||
{
|
||||
library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
|
||||
library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.Folders);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This usually only fails if user is not authenticated.
|
||||
_logger.LogError(ex, "There was an issue fetching Library {LibraryId}", libraryId);
|
||||
_logger.LogError(ex, "[ScannerService] There was an issue fetching Library {LibraryId}", libraryId);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Beginning file scan on {LibraryName}", library.Name);
|
||||
_logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name);
|
||||
var scanner = new ParseScannedFiles(_bookService, _logger);
|
||||
var series = scanner.ScanLibrariesForSeries(library.Type, library.Folders.Select(fp => fp.Path), out var totalFiles, out var scanElapsedTime);
|
||||
|
||||
|
@ -171,24 +194,24 @@ namespace API.Services.Tasks
|
|||
}
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
UpdateLibrary(library, series);
|
||||
await UpdateLibrary(library, series);
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
"[ScannerService] Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
totalFiles, series.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, library.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCritical(
|
||||
"There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
||||
"[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
||||
}
|
||||
|
||||
await CleanupAbandonedChapters();
|
||||
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibrary, MessageFactory.ScanLibraryEvent(libraryId, "complete"));
|
||||
}
|
||||
|
||||
|
@ -212,78 +235,153 @@ namespace API.Services.Tasks
|
|||
_logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp);
|
||||
}
|
||||
|
||||
private void UpdateLibrary(Library library, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
private async Task UpdateLibrary(Library library, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
{
|
||||
if (parsedSeries == null) throw new ArgumentNullException(nameof(parsedSeries));
|
||||
if (parsedSeries == null) return;
|
||||
|
||||
// First, remove any series that are not in parsedSeries list
|
||||
var missingSeries = FindSeriesNotOnDisk(library.Series, parsedSeries).ToList();
|
||||
library.Series = RemoveMissingSeries(library.Series, missingSeries, out var removeCount);
|
||||
if (removeCount > 0)
|
||||
// Library contains no Series, so we need to fetch series in groups of ChunkSize
|
||||
var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var totalTime = 0L;
|
||||
|
||||
// Update existing series
|
||||
_logger.LogDebug("[ScannerService] Updating existing series");
|
||||
for (var chunk = 0; chunk <= chunkInfo.TotalChunks; chunk++)
|
||||
{
|
||||
_logger.LogInformation("Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount);
|
||||
foreach (var s in missingSeries)
|
||||
{
|
||||
_logger.LogDebug("Removed {SeriesName} ({Format})", s.Name, s.Format);
|
||||
}
|
||||
totalTime += stopwatch.ElapsedMilliseconds;
|
||||
stopwatch.Restart();
|
||||
_logger.LogDebug($"[ScannerService] Processing chunk {chunk} / {chunkInfo.TotalChunks} with size {chunkInfo.ChunkSize} Series ({chunk * chunkInfo.ChunkSize} - {(chunk + 1) * chunkInfo.ChunkSize}");
|
||||
var nonLibrarySeries = await _unitOfWork.SeriesRepository.GetFullSeriesForLibraryIdAsync(library.Id, new UserParams()
|
||||
{
|
||||
PageNumber = chunk,
|
||||
PageSize = chunkInfo.ChunkSize
|
||||
});
|
||||
|
||||
// First, remove any series that are not in parsedSeries list
|
||||
var missingSeries = FindSeriesNotOnDisk(nonLibrarySeries, parsedSeries).ToList();
|
||||
|
||||
foreach (var missing in missingSeries)
|
||||
{
|
||||
_unitOfWork.SeriesRepository.Remove(missing);
|
||||
}
|
||||
|
||||
var cleanedSeries = RemoveMissingSeries(nonLibrarySeries, missingSeries, out var removeCount);
|
||||
if (removeCount > 0)
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] Removed {RemoveMissingSeries} series that are no longer on disk:", removeCount);
|
||||
foreach (var s in missingSeries)
|
||||
{
|
||||
_logger.LogDebug("[ScannerService] Removed {SeriesName} ({Format})", s.Name, s.Format);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we only have to deal with series that exist on disk. Let's recalculate the volumes for each series
|
||||
var librarySeries = cleanedSeries.ToList();
|
||||
Parallel.ForEach(librarySeries, (series) => { UpdateSeries(series, parsedSeries); });
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
_logger.LogInformation(
|
||||
"[ScannerService] Processed {SeriesStart} - {SeriesEnd} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
chunk * chunkInfo.ChunkSize, (chunk + 1) * chunkInfo.ChunkSize, totalTime, library.Name);
|
||||
|
||||
// Emit any series removed
|
||||
foreach (var missing in missingSeries)
|
||||
{
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesRemoved, MessageFactory.SeriesRemovedEvent(missing.Id, missing.Name, library.Id));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add new series that have parsedInfos
|
||||
_logger.LogDebug("[ScannerService] Adding new series");
|
||||
var newSeries = new List<Series>();
|
||||
var allSeries = (await _unitOfWork.SeriesRepository.GetSeriesForLibraryIdAsync(library.Id)).ToList();
|
||||
foreach (var (key, infos) in parsedSeries)
|
||||
{
|
||||
// Key is normalized already
|
||||
Series existingSeries;
|
||||
try
|
||||
{
|
||||
existingSeries = library.Series.SingleOrDefault(s =>
|
||||
(s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName)
|
||||
&& (s.Format == key.Format || s.Format == MangaFormat.Unknown));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogCritical(e, "There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it", key.NormalizedName);
|
||||
var duplicateSeries = library.Series.Where(s => s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName).ToList();
|
||||
foreach (var series in duplicateSeries)
|
||||
{
|
||||
_logger.LogCritical("{Key} maps with {Series}", key.Name, series.OriginalName);
|
||||
}
|
||||
Series existingSeries;
|
||||
try
|
||||
{
|
||||
existingSeries = allSeries.SingleOrDefault(s =>
|
||||
(s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName)
|
||||
&& (s.Format == key.Format || s.Format == MangaFormat.Unknown));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogCritical(e, "[ScannerService] There are multiple series that map to normalized key {Key}. You can manually delete the entity via UI and rescan to fix it. This will be skipped", key.NormalizedName);
|
||||
var duplicateSeries = allSeries.Where(s => s.NormalizedName == key.NormalizedName || Parser.Parser.Normalize(s.OriginalName) == key.NormalizedName).ToList();
|
||||
foreach (var series in duplicateSeries)
|
||||
{
|
||||
_logger.LogCritical("[ScannerService] Duplicate Series Found: {Key} maps with {Series}", key.Name, series.OriginalName);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
if (existingSeries == null)
|
||||
{
|
||||
existingSeries = DbFactory.Series(infos[0].Series);
|
||||
existingSeries.Format = key.Format;
|
||||
library.Series.Add(existingSeries);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
existingSeries.NormalizedName = Parser.Parser.Normalize(existingSeries.Name);
|
||||
existingSeries.OriginalName ??= infos[0].Series;
|
||||
existingSeries.Metadata ??= DbFactory.SeriesMetadata(new List<CollectionTag>());
|
||||
existingSeries.Format = key.Format;
|
||||
if (existingSeries != null) continue;
|
||||
|
||||
existingSeries = DbFactory.Series(infos[0].Series);
|
||||
existingSeries.Format = key.Format;
|
||||
newSeries.Add(existingSeries);
|
||||
}
|
||||
|
||||
// Now, we only have to deal with series that exist on disk. Let's recalculate the volumes for each series
|
||||
var librarySeries = library.Series.ToList();
|
||||
Parallel.ForEach(librarySeries, (series) =>
|
||||
foreach(var series in newSeries)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Processing series {SeriesName}", series.OriginalName);
|
||||
UpdateVolumes(series, ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray());
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "There was an exception updating volumes for {SeriesName}", series.Name);
|
||||
}
|
||||
});
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
UpdateVolumes(series, ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray());
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
series.LibraryId = library.Id; // We have to manually set this since we aren't adding the series to the Library's series.
|
||||
_unitOfWork.SeriesRepository.Attach(series);
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"[ScannerService] Added {NewSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}",
|
||||
newSeries.Count, stopwatch.ElapsedMilliseconds, library.Name);
|
||||
|
||||
// Last step, remove any series that have no pages
|
||||
library.Series = library.Series.Where(s => s.Pages > 0).ToList();
|
||||
// Inform UI of new series added
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesAdded, MessageFactory.SeriesAddedEvent(series.Id, series.Name, library.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is probably not needed. Better to catch the exception.
|
||||
_logger.LogCritical(
|
||||
"[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[ScannerService] There was an exception updating volumes for {SeriesName}", series.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Series> FindSeriesNotOnDisk(ICollection<Series> existingSeries, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
private void UpdateSeries(Series series, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] Processing series {SeriesName}", series.OriginalName);
|
||||
|
||||
var parsedInfos = ParseScannedFiles.GetInfosByName(parsedSeries, series).ToArray();
|
||||
UpdateVolumes(series, parsedInfos);
|
||||
series.Pages = series.Volumes.Sum(v => v.Pages);
|
||||
|
||||
series.NormalizedName = Parser.Parser.Normalize(series.Name);
|
||||
series.Metadata ??= DbFactory.SeriesMetadata(new List<CollectionTag>());
|
||||
if (series.Format == MangaFormat.Unknown)
|
||||
{
|
||||
series.Format = parsedInfos[0].Format;
|
||||
}
|
||||
series.OriginalName ??= parsedInfos[0].Series;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[ScannerService] There was an exception updating volumes for {SeriesName}", series.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<Series> FindSeriesNotOnDisk(IEnumerable<Series> existingSeries, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)
|
||||
{
|
||||
var foundSeries = parsedSeries.Select(s => s.Key.Name).ToList();
|
||||
return existingSeries.Where(es => !es.NameInList(foundSeries) && !SeriesHasMatchingParserInfoFormat(es, parsedSeries));
|
||||
|
@ -332,7 +430,7 @@ namespace API.Services.Tasks
|
|||
/// <param name="missingSeries">Series not found on disk or can't be parsed</param>
|
||||
/// <param name="removeCount"></param>
|
||||
/// <returns>the updated existingSeries</returns>
|
||||
public static ICollection<Series> RemoveMissingSeries(ICollection<Series> existingSeries, IEnumerable<Series> missingSeries, out int removeCount)
|
||||
public static IList<Series> RemoveMissingSeries(IList<Series> existingSeries, IEnumerable<Series> missingSeries, out int removeCount)
|
||||
{
|
||||
var existingCount = existingSeries.Count;
|
||||
var missingList = missingSeries.ToList();
|
||||
|
@ -351,7 +449,7 @@ namespace API.Services.Tasks
|
|||
var startingVolumeCount = series.Volumes.Count;
|
||||
// Add new volumes and update chapters per volume
|
||||
var distinctVolumes = parsedInfos.DistinctVolumes();
|
||||
_logger.LogDebug("Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name);
|
||||
_logger.LogDebug("[ScannerService] Updating {DistinctVolumes} volumes on {SeriesName}", distinctVolumes.Count, series.Name);
|
||||
foreach (var volumeNumber in distinctVolumes)
|
||||
{
|
||||
var volume = series.Volumes.SingleOrDefault(s => s.Name == volumeNumber);
|
||||
|
@ -359,9 +457,10 @@ namespace API.Services.Tasks
|
|||
{
|
||||
volume = DbFactory.Volume(volumeNumber);
|
||||
series.Volumes.Add(volume);
|
||||
_unitOfWork.VolumeRepository.Add(volume);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
|
||||
_logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
|
||||
var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray();
|
||||
UpdateChapters(volume, infos);
|
||||
volume.Pages = volume.Chapters.Sum(c => c.Pages);
|
||||
|
@ -371,23 +470,26 @@ namespace API.Services.Tasks
|
|||
var nonDeletedVolumes = series.Volumes.Where(v => parsedInfos.Select(p => p.Volumes).Contains(v.Name)).ToList();
|
||||
if (series.Volumes.Count != nonDeletedVolumes.Count)
|
||||
{
|
||||
_logger.LogDebug("Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name",
|
||||
_logger.LogDebug("[ScannerService] Removed {Count} volumes from {SeriesName} where parsed infos were not mapping with volume name",
|
||||
(series.Volumes.Count - nonDeletedVolumes.Count), series.Name);
|
||||
var deletedVolumes = series.Volumes.Except(nonDeletedVolumes);
|
||||
foreach (var volume in deletedVolumes)
|
||||
{
|
||||
var file = volume.Chapters.FirstOrDefault()?.Files.FirstOrDefault()?.FilePath ?? "no files";
|
||||
if (new FileInfo(file).Exists)
|
||||
{
|
||||
_logger.LogError("Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {File}", file);
|
||||
}
|
||||
_logger.LogDebug("Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file);
|
||||
var file = volume.Chapters.FirstOrDefault()?.Files?.FirstOrDefault()?.FilePath ?? "";
|
||||
if (!string.IsNullOrEmpty(file) && File.Exists(file))
|
||||
{
|
||||
_logger.LogError(
|
||||
"[ScannerService] Volume cleanup code was trying to remove a volume with a file still existing on disk. File: {File}",
|
||||
file);
|
||||
}
|
||||
|
||||
_logger.LogDebug("[ScannerService] Removed {SeriesName} - Volume {Volume}: {File}", series.Name, volume.Name, file);
|
||||
}
|
||||
|
||||
series.Volumes = nonDeletedVolumes;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Updated {SeriesName} volumes from {StartingVolumeCount} to {VolumeCount}",
|
||||
_logger.LogDebug("[ScannerService] Updated {SeriesName} volumes from {StartingVolumeCount} to {VolumeCount}",
|
||||
series.Name, startingVolumeCount, series.Volumes.Count);
|
||||
}
|
||||
|
||||
|
@ -417,7 +519,7 @@ namespace API.Services.Tasks
|
|||
if (chapter == null)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Adding new chapter, {Series} - Vol {Volume} Ch {Chapter}", info.Series, info.Volumes, info.Chapters);
|
||||
"[ScannerService] Adding new chapter, {Series} - Vol {Volume} Ch {Chapter}", info.Series, info.Volumes, info.Chapters);
|
||||
volume.Chapters.Add(DbFactory.Chapter(info));
|
||||
}
|
||||
else
|
||||
|
@ -454,7 +556,7 @@ namespace API.Services.Tasks
|
|||
{
|
||||
if (existingChapter.Files.Count == 0 || !parsedInfos.HasInfo(existingChapter))
|
||||
{
|
||||
_logger.LogDebug("Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series);
|
||||
_logger.LogDebug("[ScannerService] Removed chapter {Chapter} for Volume {VolumeNumber} on {SeriesName}", existingChapter.Range, volume.Name, parsedInfos[0].Series);
|
||||
volume.Chapters.Remove(existingChapter);
|
||||
}
|
||||
else
|
||||
|
@ -470,42 +572,47 @@ namespace API.Services.Tasks
|
|||
|
||||
private MangaFile CreateMangaFile(ParserInfo info)
|
||||
{
|
||||
switch (info.Format)
|
||||
MangaFile mangaFile = null;
|
||||
switch (info.Format)
|
||||
{
|
||||
case MangaFormat.Archive:
|
||||
{
|
||||
return new MangaFile()
|
||||
mangaFile = new MangaFile()
|
||||
{
|
||||
FilePath = info.FullFilePath,
|
||||
Format = info.Format,
|
||||
Pages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath)
|
||||
};
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Pdf:
|
||||
case MangaFormat.Epub:
|
||||
{
|
||||
return new MangaFile()
|
||||
mangaFile = new MangaFile()
|
||||
{
|
||||
FilePath = info.FullFilePath,
|
||||
Format = info.Format,
|
||||
Pages = _bookService.GetNumberOfPages(info.FullFilePath)
|
||||
};
|
||||
break;
|
||||
}
|
||||
case MangaFormat.Image:
|
||||
{
|
||||
return new MangaFile()
|
||||
{
|
||||
FilePath = info.FullFilePath,
|
||||
Format = info.Format,
|
||||
Pages = 1
|
||||
};
|
||||
mangaFile = new MangaFile()
|
||||
{
|
||||
FilePath = info.FullFilePath,
|
||||
Format = info.Format,
|
||||
Pages = 1
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_logger.LogWarning("[Scanner] Ignoring {Filename}. File type is not supported", info.Filename);
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
mangaFile?.UpdateLastModified();
|
||||
return mangaFile;
|
||||
}
|
||||
|
||||
private void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info)
|
||||
|
@ -515,20 +622,31 @@ namespace API.Services.Tasks
|
|||
if (existingFile != null)
|
||||
{
|
||||
existingFile.Format = info.Format;
|
||||
if (existingFile.HasFileBeenModified() || existingFile.Pages == 0)
|
||||
if (!existingFile.HasFileBeenModified() && existingFile.Pages != 0) return;
|
||||
switch (existingFile.Format)
|
||||
{
|
||||
existingFile.Pages = (existingFile.Format == MangaFormat.Epub || existingFile.Format == MangaFormat.Pdf)
|
||||
? _bookService.GetNumberOfPages(info.FullFilePath)
|
||||
: _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
|
||||
case MangaFormat.Epub:
|
||||
case MangaFormat.Pdf:
|
||||
existingFile.Pages = _bookService.GetNumberOfPages(info.FullFilePath);
|
||||
break;
|
||||
case MangaFormat.Image:
|
||||
existingFile.Pages = 1;
|
||||
break;
|
||||
case MangaFormat.Unknown:
|
||||
existingFile.Pages = 0;
|
||||
break;
|
||||
case MangaFormat.Archive:
|
||||
existingFile.Pages = _archiveService.GetNumberOfPagesFromArchive(info.FullFilePath);
|
||||
break;
|
||||
}
|
||||
existingFile.LastModified = File.GetLastWriteTime(info.FullFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var file = CreateMangaFile(info);
|
||||
if (file != null)
|
||||
{
|
||||
chapter.Files.Add(file);
|
||||
}
|
||||
if (file == null) return;
|
||||
|
||||
chapter.Files.Add(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue