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:
parent
fd6925b126
commit
82b5b599e0
50 changed files with 1928 additions and 234 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue