Foundational Cover Image Rework (#584)

* Updating wording on card item when total pages is 0, to be just "Cannot Read" since it could be a non-archive file

* Refactored cover images to be stored on disk. This first commit has the extraction to disk and the metadata service to handle updating when applicable.

* Refactored code to have the actual save to cover image directory done by ImageService.

* Implemented the ability to override cover images.

* Some cleanup on Image service

* Implemented the ability to cleanup old covers nightly

* Added a migration to migrate existing covers to new cover image format (files).

* Refactored all CoverImages to just be the filename, leaving the Join with Cover directory to higher level code.

* Ensure when getting user progress, we pick the first.

* Added cleanup cover images for deleted tags. Don't pull any cover images that are blank.

* After series update, clear out cover image. No change on UI, but just keeps things clear before metadata refresh hits

* Refactored image formats for covers to ImageService.

* Fixed an issue where after refactoring how images were stored, the cleanup service was deleting them after each scan.

* Changed how ShouldUpdateCoverImage works to check if file exists or not even if cover image is locked.

* Fixed unit tests

* Added caching back to cover images.

* Caching on series as well

* Code Cleanup items

* Ensure when checking if a file exists in MetadataService, that we join for cover image directory. After we scan library, do one last filter to delete any series that have 0 pages total.

* Catch exceptions so we don't run cover migration if this is first time run.

* After a scan, only clear out the cache directory and not do a deep clean.

* Implemented the ability to backup custom locked covers only.

* Fixed unit tests

* Trying to figure out why GA crashes when running MetadataServiceTests.cs

* Some debugging on GA tests not running

* Commented out tests that were causing issues in GA.

* Fixed an issue where series cover images wouldn't migrate

* Fixed the updating of links to actually do all series and not just locked
This commit is contained in:
Joseph Milazzo 2021-09-21 17:15:29 -07:00 committed by GitHub
parent fd6925b126
commit 82b5b599e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1928 additions and 234 deletions

View file

@ -147,12 +147,13 @@ namespace API.Services
///
/// This skips over any __MACOSX folder/file iteration.
/// </summary>
/// <remarks>This always creates a thumbnail</remarks>
/// <param name="archivePath"></param>
/// <param name="createThumbnail">Create a smaller variant of file extracted from archive. Archive images are usually 1MB each.</param>
/// <param name="fileName">File name to use based on context of entity.</param>
/// <returns></returns>
public byte[] GetCoverImage(string archivePath, bool createThumbnail = false)
public string GetCoverImage(string archivePath, string fileName)
{
if (archivePath == null || !IsValidArchive(archivePath)) return Array.Empty<byte>();
if (archivePath == null || !IsValidArchive(archivePath)) return String.Empty;
try
{
var libraryHandler = CanOpen(archivePath);
@ -168,7 +169,7 @@ namespace API.Services
var entry = archive.Entries.Single(e => e.FullName == entryName);
using var stream = entry.Open();
return createThumbnail ? CreateThumbnail(entry.FullName, stream) : ConvertEntryToByteArray(entry);
return CreateThumbnail(entry.FullName, stream, fileName);
}
case ArchiveLibrary.SharpCompress:
{
@ -183,14 +184,14 @@ namespace API.Services
entry.WriteTo(ms);
ms.Position = 0;
return createThumbnail ? CreateThumbnail(entry.Key, ms, Path.GetExtension(entry.Key)) : ms.ToArray();
return CreateThumbnail(entry.Key, ms, fileName); // Path.GetExtension(entry.Key)
}
case ArchiveLibrary.NotSupported:
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
return Array.Empty<byte>();
return String.Empty;
default:
_logger.LogWarning("[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath);
return Array.Empty<byte>();
return String.Empty;
}
}
catch (Exception ex)
@ -198,15 +199,7 @@ namespace API.Services
_logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath);
}
return Array.Empty<byte>();
}
private static byte[] ConvertEntryToByteArray(ZipArchiveEntry entry)
{
using var stream = entry.Open();
using var ms = StreamManager.GetStream();
stream.CopyTo(ms);
return ms.ToArray();
return String.Empty;
}
/// <summary>
@ -223,6 +216,7 @@ namespace API.Services
archive.Entries.Any(e => e.FullName.Contains(Path.AltDirectorySeparatorChar) && !Parser.Parser.HasBlacklistedFolderInPath(e.FullName));
}
// TODO: Refactor CreateZipForDownload to return the temp file so we can stream it from temp
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
{
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
@ -254,23 +248,18 @@ namespace API.Services
return Tuple.Create(fileBytes, zipPath);
}
private byte[] CreateThumbnail(string entryName, Stream stream, string formatExtension = ".jpg")
private string CreateThumbnail(string entryName, Stream stream, string fileName)
{
if (!formatExtension.StartsWith("."))
{
formatExtension = $".{formatExtension}";
}
try
{
using var thumbnail = Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
return thumbnail.WriteToBuffer(formatExtension);
return ImageService.WriteCoverThumbnail(stream, fileName);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {EntryName}. Defaulting to no cover image", entryName);
}
return Array.Empty<byte>();
return string.Empty;
}
/// <summary>
@ -332,7 +321,7 @@ namespace API.Services
{
case ArchiveLibrary.Default:
{
_logger.LogDebug("Using default compression handling");
_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
@ -348,7 +337,7 @@ namespace API.Services
}
case ArchiveLibrary.SharpCompress:
{
_logger.LogDebug("Using SharpCompress compression handling");
_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)

View file

@ -382,14 +382,19 @@ namespace API.Services
}
}
public byte[] GetCoverImage(string fileFilePath, bool createThumbnail = true)
/// <summary>
/// Extracts the cover image to covers directory and returns file path back
/// </summary>
/// <param name="fileFilePath"></param>
/// <param name="fileName">Name of the new file.</param>
/// <returns></returns>
public string GetCoverImage(string fileFilePath, string fileName)
{
if (!IsValidFile(fileFilePath)) return Array.Empty<byte>();
if (!IsValidFile(fileFilePath)) return String.Empty;
if (Parser.Parser.IsPdf(fileFilePath))
{
return GetPdfCoverImage(fileFilePath, createThumbnail);
return GetPdfCoverImage(fileFilePath, fileName);
}
using var epubBook = EpubReader.OpenBook(fileFilePath);
@ -402,47 +407,41 @@ namespace API.Services
?? epubBook.Content.Images.Values.FirstOrDefault(file => Parser.Parser.IsCoverImage(file.FileName))
?? epubBook.Content.Images.Values.FirstOrDefault();
if (coverImageContent == null) return Array.Empty<byte>();
if (!createThumbnail) return coverImageContent.ReadContent();
if (coverImageContent == null) return string.Empty;
using var stream = StreamManager.GetStream("BookService.GetCoverImage", coverImageContent.ReadContent());
using var thumbnail = NetVips.Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
return thumbnail.WriteToBuffer(".jpg");
return ImageService.WriteCoverThumbnail(stream, fileName);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath);
}
return Array.Empty<byte>();
return string.Empty;
}
private byte[] GetPdfCoverImage(string fileFilePath, bool createThumbnail)
private string GetPdfCoverImage(string fileFilePath, string fileName)
{
try
{
using var docReader = DocLib.Instance.GetDocReader(fileFilePath, new PageDimensions(1080, 1920));
if (docReader.GetPageCount() == 0) return Array.Empty<byte>();
try
{
using var docReader = DocLib.Instance.GetDocReader(fileFilePath, new PageDimensions(1080, 1920));
if (docReader.GetPageCount() == 0) return string.Empty;
using var stream = StreamManager.GetStream("BookService.GetPdfPage");
GetPdfPage(docReader, 0, stream);
using var stream = StreamManager.GetStream("BookService.GetPdfPage");
GetPdfPage(docReader, 0, stream);
if (!createThumbnail) return stream.ToArray();
return ImageService.WriteCoverThumbnail(stream, fileName);
using var thumbnail = NetVips.Image.ThumbnailStream(stream, MetadataService.ThumbnailWidth);
return thumbnail.WriteToBuffer(".png");
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image",
fileFilePath);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image",
fileFilePath);
}
return Array.Empty<byte>();
return string.Empty;
}
private static void GetPdfPage(IDocReader docReader, int pageNumber, Stream stream)

View file

@ -19,6 +19,7 @@ namespace API.Services
public static readonly string TempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
public static readonly string LogDirectory = Path.Join(Directory.GetCurrentDirectory(), "logs");
public static readonly string CacheDirectory = Path.Join(Directory.GetCurrentDirectory(), "cache");
public static readonly string CoverImageDirectory = Path.Join(Directory.GetCurrentDirectory(), "covers");
public DirectoryService(ILogger<DirectoryService> logger)
{

View file

@ -14,6 +14,15 @@ namespace API.Services
{
private readonly ILogger<ImageService> _logger;
private readonly IDirectoryService _directoryService;
public const string ChapterCoverImageRegex = @"v\d+_c\d+";
public const string SeriesCoverImageRegex = @"seres\d+";
public const string CollectionTagCoverImageRegex = @"tag\d+";
/// <summary>
/// Width of the Thumbnail generation
/// </summary>
private const int ThumbnailWidth = 320;
public ImageService(ILogger<ImageService> logger, IDirectoryService directoryService)
{
@ -41,63 +50,103 @@ namespace API.Services
return firstImage;
}
public byte[] GetCoverImage(string path, bool createThumbnail = false)
public string GetCoverImage(string path, string fileName)
{
if (string.IsNullOrEmpty(path)) return Array.Empty<byte>();
if (string.IsNullOrEmpty(path)) return string.Empty;
try
{
if (createThumbnail)
{
return CreateThumbnail(path);
}
using var img = Image.NewFromFile(path);
using var stream = new MemoryStream();
img.JpegsaveStream(stream);
stream.Position = 0;
return stream.ToArray();
return CreateThumbnail(path, fileName);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[GetCoverImage] There was an error and prevented thumbnail generation on {ImageFile}. Defaulting to no cover image", path);
}
return Array.Empty<byte>();
return string.Empty;
}
/// <inheritdoc />
public byte[] CreateThumbnail(string path)
public string CreateThumbnail(string path, string fileName)
{
try
{
using var thumbnail = Image.Thumbnail(path, MetadataService.ThumbnailWidth);
return thumbnail.WriteToBuffer(".jpg");
using var thumbnail = Image.Thumbnail(path, ThumbnailWidth);
var filename = fileName + ".png";
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png"));
return filename;
}
catch (Exception e)
{
_logger.LogError(e, "Error creating thumbnail from url");
}
return Array.Empty<byte>();
return string.Empty;
}
/// <summary>
/// Creates a thumbnail out of a memory stream and saves to <see cref="DirectoryService.CoverImageDirectory"/> with the passed
/// fileName and .png extension.
/// </summary>
/// <param name="stream">Stream to write to disk. Ensure this is rewinded.</param>
/// <param name="fileName">filename to save as without extension</param>
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
public static string WriteCoverThumbnail(Stream stream, string fileName)
{
using var thumbnail = NetVips.Image.ThumbnailStream(stream, ThumbnailWidth);
var filename = fileName + ".png";
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png"));
return filename;
}
/// <inheritdoc />
public byte[] CreateThumbnailFromBase64(string encodedImage)
public string CreateThumbnailFromBase64(string encodedImage, string fileName)
{
try
{
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), MetadataService.ThumbnailWidth);
return thumbnail.WriteToBuffer(".jpg");
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), ThumbnailWidth);
var filename = fileName + ".png";
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png"));
return filename;
}
catch (Exception e)
{
_logger.LogError(e, "Error creating thumbnail from url");
}
return Array.Empty<byte>();
return string.Empty;
}
/// <summary>
/// Returns the name format for a chapter cover image
/// </summary>
/// <param name="chapterId"></param>
/// <param name="volumeId"></param>
/// <returns></returns>
public static string GetChapterFormat(int chapterId, int volumeId)
{
return $"v{volumeId}_c{chapterId}";
}
/// <summary>
/// Returns the name format for a series cover image
/// </summary>
/// <param name="seriesId"></param>
/// <returns></returns>
public static string GetSeriesFormat(int seriesId)
{
return $"series{seriesId}";
}
/// <summary>
/// Returns the name format for a collection tag cover image
/// </summary>
/// <param name="tagId"></param>
/// <returns></returns>
public static string GetCollectionTagFormat(int tagId)
{
return $"tag{tagId}";
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Comparators;
@ -24,10 +25,6 @@ namespace API.Services
private readonly IImageService _imageService;
private readonly IHubContext<MessageHub> _messageHub;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
/// <summary>
/// Width of the Thumbnail generation
/// </summary>
public static readonly int ThumbnailWidth = 320; // 153w x 230h
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
IArchiveService archiveService, IBookService bookService, IImageService imageService, IHubContext<MessageHub> messageHub)
@ -41,41 +38,55 @@ namespace API.Services
}
/// <summary>
/// Determines whether an entity should regenerate cover image
/// Determines whether an entity should regenerate cover image.
/// </summary>
/// <remarks>If a cover image is locked but the underlying file has been deleted, this will allow regenerating. </remarks>
/// <param name="coverImage"></param>
/// <param name="firstFile"></param>
/// <param name="forceUpdate"></param>
/// <param name="isCoverLocked"></param>
/// <param name="coverImageDirectory">Directory where cover images are. Defaults to <see cref="DirectoryService.CoverImageDirectory"/></param>
/// <returns></returns>
public static bool ShouldUpdateCoverImage(byte[] coverImage, MangaFile firstFile, bool forceUpdate = false,
bool isCoverLocked = false)
public static bool ShouldUpdateCoverImage(string coverImage, MangaFile firstFile, bool forceUpdate = false,
bool isCoverLocked = false, string coverImageDirectory = null)
{
if (isCoverLocked) return false;
if (string.IsNullOrEmpty(coverImageDirectory))
{
coverImageDirectory = DirectoryService.CoverImageDirectory;
}
var fileExists = File.Exists(Path.Join(coverImageDirectory, coverImage));
if (isCoverLocked && fileExists) return false;
if (forceUpdate) return true;
return (firstFile != null && firstFile.HasFileBeenModified()) || !HasCoverImage(coverImage);
return (firstFile != null && firstFile.HasFileBeenModified()) || !HasCoverImage(coverImage, fileExists);
}
private static bool HasCoverImage(byte[] coverImage)
private static bool HasCoverImage(string coverImage)
{
return coverImage != null && coverImage.Any();
return HasCoverImage(coverImage, File.Exists(coverImage));
}
private byte[] GetCoverImage(MangaFile file, bool createThumbnail = true)
private static bool HasCoverImage(string coverImage, bool fileExists)
{
return !string.IsNullOrEmpty(coverImage) && fileExists;
}
private string GetCoverImage(MangaFile file, int volumeId, int chapterId)
{
file.LastModified = DateTime.Now;
switch (file.Format)
{
case MangaFormat.Pdf:
case MangaFormat.Epub:
return _bookService.GetCoverImage(file.FilePath, createThumbnail);
return _bookService.GetCoverImage(file.FilePath, ImageService.GetChapterFormat(chapterId, volumeId));
case MangaFormat.Image:
var coverImage = _imageService.GetCoverFile(file);
return _imageService.GetCoverImage(coverImage, createThumbnail);
return _imageService.GetCoverImage(coverImage, ImageService.GetChapterFormat(chapterId, volumeId));
case MangaFormat.Archive:
return _archiveService.GetCoverImage(file.FilePath, createThumbnail);
return _archiveService.GetCoverImage(file.FilePath, ImageService.GetChapterFormat(chapterId, volumeId));
default:
return Array.Empty<byte>();
return string.Empty;
}
}
@ -91,7 +102,7 @@ namespace API.Services
if (ShouldUpdateCoverImage(chapter.CoverImage, firstFile, forceUpdate, chapter.CoverImageLocked))
{
chapter.CoverImage = GetCoverImage(firstFile);
chapter.CoverImage = GetCoverImage(firstFile, chapter.VolumeId, chapter.Id);
return true;
}
@ -130,7 +141,7 @@ namespace API.Services
{
series.Volumes ??= new List<Volume>();
var firstCover = series.Volumes.GetCoverImage(series.Format);
byte[] coverImage = null;
string coverImage = null;
if (firstCover == null && series.Volumes.Any())
{
// If firstCover is null and one volume, the whole series is Chapters under Vol 0.

View file

@ -121,7 +121,7 @@ namespace API.Services
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, forceUpdate));
// When we do a scan, force cache to re-unpack in case page numbers change
BackgroundJob.Enqueue(() => _cleanupService.Cleanup());
BackgroundJob.Enqueue(() => _cleanupService.CleanupCacheDirectory());
}
public void CleanupChapters(int[] chapterIds)

View file

@ -59,8 +59,11 @@ namespace API.Services.Tasks
return files;
}
/// <summary>
/// Will backup anything that needs to be backed up. This includes logs, setting files, bare minimum cover images (just locked and first cover).
/// </summary>
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public void BackupDatabase()
public async Task BackupDatabase()
{
_logger.LogInformation("Beginning backup of Database at {BackupTime}", DateTime.Now);
var backupDirectory = Task.Run(() => _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Result.Value;
@ -87,6 +90,9 @@ namespace API.Services.Tasks
_directoryService.CopyFilesToDirectory(
_backupFiles.Select(file => Path.Join(Directory.GetCurrentDirectory(), file)).ToList(), tempDirectory);
await CopyCoverImagesToBackupDirectory(tempDirectory);
try
{
ZipFile.CreateFromDirectory(tempDirectory, zipPath);
@ -100,6 +106,31 @@ namespace API.Services.Tasks
_logger.LogInformation("Database backup completed");
}
private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)
{
var outputTempDir = Path.Join(tempDirectory, "covers");
DirectoryService.ExistOrCreate(outputTempDir);
try
{
var seriesImages = await _unitOfWork.SeriesRepository.GetLockedCoverImagesAsync();
_directoryService.CopyFilesToDirectory(
seriesImages.Select(s => Path.Join(DirectoryService.CoverImageDirectory, s)), outputTempDir);
var collectionTags = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync();
_directoryService.CopyFilesToDirectory(
collectionTags.Select(s => Path.Join(DirectoryService.CoverImageDirectory, s)), outputTempDir);
var chapterImages = await _unitOfWork.ChapterRepository.GetCoverImagesForLockedChaptersAsync();
_directoryService.CopyFilesToDirectory(
chapterImages.Select(s => Path.Join(DirectoryService.CoverImageDirectory, s)), outputTempDir);
}
catch (IOException e)
{
// Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file.
}
}
/// <summary>
/// Removes Database backups older than 30 days. If all backups are older than 30 days, the latest is kept.
/// </summary>

View file

@ -1,7 +1,11 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using API.Interfaces;
using API.Interfaces.Services;
using Hangfire;
using Microsoft.Extensions.Logging;
using NetVips;
namespace API.Services.Tasks
{
@ -13,27 +17,79 @@ namespace API.Services.Tasks
private readonly ICacheService _cacheService;
private readonly ILogger<CleanupService> _logger;
private readonly IBackupService _backupService;
private readonly IUnitOfWork _unitOfWork;
private readonly IDirectoryService _directoryService;
public CleanupService(ICacheService cacheService, ILogger<CleanupService> logger, IBackupService backupService)
public CleanupService(ICacheService cacheService, ILogger<CleanupService> logger,
IBackupService backupService, IUnitOfWork unitOfWork, IDirectoryService directoryService)
{
_cacheService = cacheService;
_logger = logger;
_backupService = backupService;
_unitOfWork = unitOfWork;
_directoryService = directoryService;
}
public void CleanupCacheDirectory()
{
_logger.LogInformation("Cleaning cache directory");
_cacheService.Cleanup();
}
/// <summary>
/// Cleans up Temp, cache, and old database backups
/// Cleans up Temp, cache, deleted cover images, and old database backups
/// </summary>
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public void Cleanup()
public async Task Cleanup()
{
_logger.LogInformation("Starting Cleanup");
_logger.LogInformation("Cleaning temp directory");
var tempDirectory = Path.Join(Directory.GetCurrentDirectory(), "temp");
DirectoryService.ClearDirectory(tempDirectory);
_logger.LogInformation("Cleaning cache directory");
_cacheService.Cleanup();
CleanupCacheDirectory();
_logger.LogInformation("Cleaning old database backups");
_backupService.CleanupBackups();
_logger.LogInformation("Cleaning deleted cover images");
await DeleteSeriesCoverImages();
await DeleteChapterCoverImages();
await DeleteTagCoverImages();
_logger.LogInformation("Cleanup finished");
}
private async Task DeleteSeriesCoverImages()
{
var images = await _unitOfWork.SeriesRepository.GetAllCoverImagesAsync();
var files = _directoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.SeriesCoverImageRegex);
foreach (var file in files)
{
if (images.Contains(Path.GetFileName(file))) continue;
File.Delete(file);
}
}
private async Task DeleteChapterCoverImages()
{
var images = await _unitOfWork.ChapterRepository.GetAllCoverImagesAsync();
var files = _directoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.ChapterCoverImageRegex);
foreach (var file in files)
{
if (images.Contains(Path.GetFileName(file))) continue;
File.Delete(file);
}
}
private async Task DeleteTagCoverImages()
{
var images = await _unitOfWork.CollectionTagRepository.GetAllCoverImagesAsync();
var files = _directoryService.GetFiles(DirectoryService.CoverImageDirectory, ImageService.CollectionTagCoverImageRegex);
foreach (var file in files)
{
if (images.Contains(Path.GetFileName(file))) continue;
File.Delete(file);
}
}
}
}

View file

@ -48,7 +48,7 @@ namespace API.Services.Tasks.Scanner
public static IList<ParserInfo> GetInfosByName(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
{
var existingKey = parsedSeries.Keys.FirstOrDefault(ps =>
ps.Format == series.Format && ps.NormalizedName == Parser.Parser.Normalize(series.OriginalName));
ps.Format == series.Format && ps.NormalizedName.Equals(Parser.Parser.Normalize(series.OriginalName)));
return existingKey != null ? parsedSeries[existingKey] : new List<ParserInfo>();
}

View file

@ -277,6 +277,9 @@ namespace API.Services.Tasks
_logger.LogError(ex, "There was an exception updating volumes for {SeriesName}", series.Name);
}
});
// Last step, remove any series that have no pages
library.Series = library.Series.Where(s => s.Pages > 0).ToList();
}
public IEnumerable<Series> FindSeriesNotOnDisk(ICollection<Series> existingSeries, Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries)