AVIF Support & Much More! (#1992)
* Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures.
This commit is contained in:
parent
c1989e2819
commit
70690b747e
73 changed files with 778 additions and 566 deletions
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Xml.Serialization;
|
||||
using API.Archive;
|
||||
using API.Data.Metadata;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Services.Tasks;
|
||||
using Kavita.Common;
|
||||
|
|
@ -20,7 +21,7 @@ public interface IArchiveService
|
|||
{
|
||||
void ExtractArchive(string archivePath, string extractPath);
|
||||
int GetNumberOfPagesFromArchive(string archivePath);
|
||||
string GetCoverImage(string archivePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format);
|
||||
bool IsValidArchive(string archivePath);
|
||||
ComicInfo? GetComicInfo(string archivePath);
|
||||
ArchiveLibrary CanOpen(string archivePath);
|
||||
|
|
@ -201,9 +202,9 @@ public class ArchiveService : IArchiveService
|
|||
/// <param name="archivePath"></param>
|
||||
/// <param name="fileName">File name to use based on context of entity.</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <param name="saveAsWebP">When saving the file, use WebP encoding instead of PNG</param>
|
||||
/// <param name="encodeFormat">When saving the file, use encoding</param>
|
||||
/// <returns></returns>
|
||||
public string GetCoverImage(string archivePath, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (archivePath == null || !IsValidArchive(archivePath)) return string.Empty;
|
||||
try
|
||||
|
|
@ -219,7 +220,7 @@ public class ArchiveService : IArchiveService
|
|||
var entry = archive.Entries.Single(e => e.FullName == entryName);
|
||||
|
||||
using var stream = entry.Open();
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
case ArchiveLibrary.SharpCompress:
|
||||
{
|
||||
|
|
@ -230,7 +231,7 @@ public class ArchiveService : IArchiveService
|
|||
var entry = archive.Entries.Single(e => e.Key == entryName);
|
||||
|
||||
using var stream = entry.OpenEntryStream();
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
case ArchiveLibrary.NotSupported:
|
||||
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
|
||||
|
|
@ -426,7 +427,7 @@ public class ArchiveService : IArchiveService
|
|||
{
|
||||
entry.WriteToDirectory(extractPath, new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true, // Don't flatten, let the flatterner ensure correct order of nested folders
|
||||
ExtractFullPath = true, // Don't flatten, let the flattener ensure correct order of nested folders
|
||||
Overwrite = false
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace API.Services;
|
|||
public interface IBookService
|
||||
{
|
||||
int GetNumberOfPages(string filePath);
|
||||
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
ComicInfo? GetComicInfo(string filePath);
|
||||
ParserInfo? ParseInfo(string filePath);
|
||||
/// <summary>
|
||||
|
|
@ -1062,15 +1062,15 @@ public class BookService : IBookService
|
|||
/// <param name="fileFilePath"></param>
|
||||
/// <param name="fileName">Name of the new file.</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <param name="saveAsWebP">When saving the file, use WebP encoding instead of PNG</param>
|
||||
/// <param name="encodeFormat">When saving the file, use encoding</param>
|
||||
/// <returns></returns>
|
||||
public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (!IsValidFile(fileFilePath)) return string.Empty;
|
||||
|
||||
if (Parser.IsPdf(fileFilePath))
|
||||
{
|
||||
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, saveAsWebP);
|
||||
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
|
||||
using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions);
|
||||
|
|
@ -1085,7 +1085,7 @@ public class BookService : IBookService
|
|||
if (coverImageContent == null) return string.Empty;
|
||||
using var stream = coverImageContent.GetContentStream();
|
||||
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -1098,7 +1098,7 @@ public class BookService : IBookService
|
|||
}
|
||||
|
||||
|
||||
private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, bool saveAsWebP)
|
||||
private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -1108,7 +1108,7 @@ public class BookService : IBookService
|
|||
using var stream = StreamManager.GetStream("BookService.GetPdfPage");
|
||||
GetPdfPage(docReader, 0, stream);
|
||||
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, saveAsWebP);
|
||||
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using API.Data;
|
|||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -19,9 +18,6 @@ public interface IBookmarkService
|
|||
Task<bool> BookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto, string imageToBookmark);
|
||||
Task<bool> RemoveBookmarkPage(AppUser userWithBookmarks, BookmarkDto bookmarkDto);
|
||||
Task<IEnumerable<string>> GetBookmarkFilesById(IEnumerable<int> bookmarkIds);
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllBookmarkToWebP();
|
||||
Task ConvertAllCoverToWebP();
|
||||
}
|
||||
|
||||
public class BookmarkService : IBookmarkService
|
||||
|
|
@ -30,17 +26,15 @@ public class BookmarkService : IBookmarkService
|
|||
private readonly ILogger<BookmarkService> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IMediaConversionService _mediaConversionService;
|
||||
|
||||
public BookmarkService(ILogger<BookmarkService> logger, IUnitOfWork unitOfWork,
|
||||
IDirectoryService directoryService, IImageService imageService, IEventHub eventHub)
|
||||
IDirectoryService directoryService, IMediaConversionService mediaConversionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_directoryService = directoryService;
|
||||
_imageService = imageService;
|
||||
_eventHub = eventHub;
|
||||
_mediaConversionService = mediaConversionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -77,21 +71,25 @@ public class BookmarkService : IBookmarkService
|
|||
/// This is a job that runs after a bookmark is saved
|
||||
/// </summary>
|
||||
/// <remarks>This must be public</remarks>
|
||||
public async Task ConvertBookmarkToWebP(int bookmarkId)
|
||||
public async Task ConvertBookmarkToEncoding(int bookmarkId)
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var convertBookmarkToWebP =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (!convertBookmarkToWebP) return;
|
||||
if (encodeFormat == EncodeFormat.PNG)
|
||||
{
|
||||
_logger.LogError("Cannot convert media to PNG");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the bookmark still exists
|
||||
var bookmark = await _unitOfWork.UserRepository.GetBookmarkAsync(bookmarkId);
|
||||
if (bookmark == null) return;
|
||||
|
||||
bookmark.FileName = await SaveAsWebP(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId));
|
||||
bookmark.FileName = await _mediaConversionService.SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat);
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
|
@ -137,10 +135,10 @@ public class BookmarkService : IBookmarkService
|
|||
_unitOfWork.UserRepository.Add(bookmark);
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (settings.ConvertBookmarkToWebP)
|
||||
if (settings.EncodeMediaAs == EncodeFormat.WEBP)
|
||||
{
|
||||
// Enqueue a task to convert the bookmark to webP
|
||||
BackgroundJob.Enqueue(() => ConvertBookmarkToWebP(bookmark.Id));
|
||||
BackgroundJob.Enqueue(() => ConvertBookmarkToEncoding(bookmark.Id));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -192,198 +190,9 @@ public class BookmarkService : IBookmarkService
|
|||
b.FileName)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a long-running job that will convert all bookmarks into WebP. Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllBookmarkToWebP()
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
|
||||
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
||||
.Where(b => !b.FileName.EndsWith(".webp")).ToList();
|
||||
|
||||
var count = 1F;
|
||||
foreach (var bookmark in bookmarks)
|
||||
{
|
||||
bookmark.FileName = await SaveAsWebP(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId));
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Converted bookmarks to WebP");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllCoverToWebP()
|
||||
{
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of all covers to webp");
|
||||
var coverDirectory = _directoryService.CoverImageDirectory;
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started));
|
||||
var chapterCovers = await _unitOfWork.ChapterRepository.GetAllChaptersWithNonWebPCovers();
|
||||
var seriesCovers = await _unitOfWork.SeriesRepository.GetAllWithNonWebPCovers();
|
||||
|
||||
var readingListCovers = await _unitOfWork.ReadingListRepository.GetAllWithNonWebPCovers();
|
||||
var libraryCovers = await _unitOfWork.LibraryRepository.GetAllWithNonWebPCovers();
|
||||
var collectionCovers = await _unitOfWork.CollectionTagRepository.GetAllWithNonWebPCovers();
|
||||
|
||||
var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count +
|
||||
libraryCovers.Count + collectionCovers.Count;
|
||||
|
||||
var count = 1F;
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of chapters");
|
||||
foreach (var chapter in chapterCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapter.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, chapter.CoverImage, coverDirectory);
|
||||
chapter.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of series");
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, series.CoverImage, coverDirectory);
|
||||
series.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of libraries");
|
||||
foreach (var library in libraryCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(library.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, library.CoverImage, coverDirectory);
|
||||
library.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of reading lists");
|
||||
foreach (var readingList in readingListCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(readingList.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, readingList.CoverImage, coverDirectory);
|
||||
readingList.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Starting conversion of collections");
|
||||
foreach (var collection in collectionCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(collection.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsWebP(coverDirectory, collection.CoverImage, coverDirectory);
|
||||
collection.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.CollectionTagRepository.Update(collection);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Started));
|
||||
count++;
|
||||
}
|
||||
|
||||
// Now null out all series and volumes that aren't webp or custom
|
||||
var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithNonWebPCovers();
|
||||
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(volume.CoverImage)) continue;
|
||||
volume.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
var nonCustomOrConvertedSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithNonWebPCovers(false);
|
||||
foreach (var series in nonCustomOrConvertedSeriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
series.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[BookmarkService] Converted covers to WebP");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts an image file, deletes original and returns the new path back
|
||||
/// </summary>
|
||||
/// <param name="imageDirectory">Full Path to where files are stored</param>
|
||||
/// <param name="filename">The file to convert</param>
|
||||
/// <param name="targetFolder">Full path to where files should be stored or any stem</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> SaveAsWebP(string imageDirectory, string filename, string targetFolder)
|
||||
{
|
||||
// This must be Public as it's used in via Hangfire as a background task
|
||||
var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename);
|
||||
var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty);
|
||||
|
||||
var newFilename = string.Empty;
|
||||
_logger.LogDebug("Converting {Source} image into WebP at {Target}", fullSourcePath, fullTargetDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
// Convert target file to webp then delete original target file and update bookmark
|
||||
|
||||
try
|
||||
{
|
||||
var targetFile = await _imageService.ConvertToWebP(fullSourcePath, fullTargetDirectory);
|
||||
var targetName = new FileInfo(targetFile).Name;
|
||||
newFilename = Path.Join(targetFolder, targetName);
|
||||
_directoryService.DeleteFiles(new[] {fullSourcePath});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert image {FilePath}", filename);
|
||||
newFilename = filename;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert image to WebP");
|
||||
}
|
||||
|
||||
return newFilename;
|
||||
}
|
||||
|
||||
private static string BookmarkStem(int userId, int seriesId, int chapterId)
|
||||
public static string BookmarkStem(int userId, int seriesId, int chapterId)
|
||||
{
|
||||
return Path.Join($"{userId}", $"{seriesId}", $"{chapterId}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using Flurl;
|
||||
using Flurl.Http;
|
||||
using HtmlAgilityPack;
|
||||
|
|
@ -16,49 +18,49 @@ namespace API.Services;
|
|||
public interface IImageService
|
||||
{
|
||||
void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1);
|
||||
string GetCoverImage(string path, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Thumbnail version of a base64 image
|
||||
/// </summary>
|
||||
/// <param name="encodedImage">base64 encoded image</param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="saveAsWebP">Convert and save as webp</param>
|
||||
/// <param name="encodeFormat">Convert and save as encoding format</param>
|
||||
/// <param name="thumbnailWidth">Width of thumbnail</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = 320);
|
||||
string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = 320);
|
||||
/// <summary>
|
||||
/// Writes out a thumbnail by stream input
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="outputDirectory"></param>
|
||||
/// <param name="saveAsWebP"></param>
|
||||
/// <param name="encodeFormat"></param>
|
||||
/// <returns></returns>
|
||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
/// <summary>
|
||||
/// Writes out a thumbnail by file path input
|
||||
/// </summary>
|
||||
/// <param name="sourceFile"></param>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="outputDirectory"></param>
|
||||
/// <param name="saveAsWebP"></param>
|
||||
/// <param name="encodeFormat"></param>
|
||||
/// <returns></returns>
|
||||
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false);
|
||||
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat);
|
||||
/// <summary>
|
||||
/// Converts the passed image to webP and outputs it in the same directory
|
||||
/// Converts the passed image to encoding and outputs it in the same directory
|
||||
/// </summary>
|
||||
/// <param name="filePath">Full path to the image to convert</param>
|
||||
/// <param name="outputPath">Where to output the file</param>
|
||||
/// <returns>File of written webp image</returns>
|
||||
Task<string> ConvertToWebP(string filePath, string outputPath);
|
||||
|
||||
/// <returns>File of written encoded image</returns>
|
||||
Task<string> ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat);
|
||||
Task<bool> IsImage(string filePath);
|
||||
Task<string> DownloadFaviconAsync(string url);
|
||||
Task<string> DownloadFaviconAsync(string url, EncodeFormat encodeFormat);
|
||||
}
|
||||
|
||||
public class ImageService : IImageService
|
||||
{
|
||||
public const string Name = "BookmarkService";
|
||||
private readonly ILogger<ImageService> _logger;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
public const string ChapterCoverImageRegex = @"v\d+_c\d+";
|
||||
|
|
@ -75,6 +77,20 @@ public class ImageService : IImageService
|
|||
/// </summary>
|
||||
public const int LibraryThumbnailWidth = 32;
|
||||
|
||||
private static readonly string[] ValidIconRelations = {
|
||||
"icon",
|
||||
"apple-touch-icon",
|
||||
"apple-touch-icon-precomposed"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A mapping of urls that need to get the icon from another url, due to strangeness (like app.plex.tv loading a black icon)
|
||||
/// </summary>
|
||||
private static readonly IDictionary<string, string> FaviconUrlMapper = new Dictionary<string, string>
|
||||
{
|
||||
["https://app.plex.tv"] = "https://plex.tv"
|
||||
};
|
||||
|
||||
public ImageService(ILogger<ImageService> logger, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
|
|
@ -96,14 +112,14 @@ public class ImageService : IImageService
|
|||
}
|
||||
}
|
||||
|
||||
public string GetCoverImage(string path, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
using var thumbnail = Image.Thumbnail(path, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
|
||||
return filename;
|
||||
}
|
||||
|
|
@ -122,12 +138,12 @@ public class ImageService : IImageService
|
|||
/// <param name="stream">Stream to write to disk. Ensure this is rewinded.</param>
|
||||
/// <param name="fileName">filename to save as without extension</param>
|
||||
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
|
||||
/// <param name="saveAsWebP">Export the file as webP otherwise will default to png</param>
|
||||
/// <param name="encodeFormat">Export the file as the passed encoding</param>
|
||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
{
|
||||
|
|
@ -137,10 +153,10 @@ public class ImageService : IImageService
|
|||
return filename;
|
||||
}
|
||||
|
||||
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, bool saveAsWebP = false)
|
||||
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat)
|
||||
{
|
||||
using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth);
|
||||
var filename = fileName + (saveAsWebP ? ".webp" : ".png");
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
{
|
||||
|
|
@ -150,11 +166,11 @@ public class ImageService : IImageService
|
|||
return filename;
|
||||
}
|
||||
|
||||
public Task<string> ConvertToWebP(string filePath, string outputPath)
|
||||
public Task<string> ConvertToEncodingFormat(string filePath, string outputPath, EncodeFormat encodeFormat)
|
||||
{
|
||||
var file = _directoryService.FileSystem.FileInfo.New(filePath);
|
||||
var fileName = file.Name.Replace(file.Extension, string.Empty);
|
||||
var outputFile = Path.Join(outputPath, fileName + ".webp");
|
||||
var outputFile = Path.Join(outputPath, fileName + encodeFormat.GetExtension());
|
||||
|
||||
using var sourceImage = Image.NewFromFile(filePath, false, Enums.Access.SequentialUnbuffered);
|
||||
sourceImage.WriteToFile(outputFile);
|
||||
|
|
@ -183,24 +199,26 @@ public class ImageService : IImageService
|
|||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> DownloadFaviconAsync(string url)
|
||||
public async Task<string> DownloadFaviconAsync(string url, EncodeFormat encodeFormat)
|
||||
{
|
||||
// Parse the URL to get the domain (including subdomain)
|
||||
var uri = new Uri(url);
|
||||
var domain = uri.Host;
|
||||
var baseUrl = uri.Scheme + "://" + uri.Host;
|
||||
|
||||
|
||||
if (FaviconUrlMapper.TryGetValue(baseUrl, out var value))
|
||||
{
|
||||
url = value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var validIconRelations = new[]
|
||||
{
|
||||
"icon",
|
||||
"apple-touch-icon",
|
||||
};
|
||||
var htmlContent = url.GetStringAsync().Result;
|
||||
var htmlDocument = new HtmlDocument();
|
||||
htmlDocument.LoadHtml(htmlContent);
|
||||
var pngLinks = htmlDocument.DocumentNode.Descendants("link")
|
||||
.Where(link => validIconRelations.Contains(link.GetAttributeValue("rel", string.Empty)))
|
||||
.Where(link => ValidIconRelations.Contains(link.GetAttributeValue("rel", string.Empty)))
|
||||
.Select(link => link.GetAttributeValue("href", string.Empty))
|
||||
.Where(href => href.EndsWith(".png") || href.EndsWith(".PNG"))
|
||||
.ToList();
|
||||
|
|
@ -228,9 +246,23 @@ public class ImageService : IImageService
|
|||
.GetStreamAsync();
|
||||
|
||||
// Create the destination file path
|
||||
var filename = $"{domain}.png";
|
||||
using var image = Image.PngloadStream(faviconStream);
|
||||
image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
var filename = $"{domain}{encodeFormat.GetExtension()}";
|
||||
switch (encodeFormat)
|
||||
{
|
||||
case EncodeFormat.PNG:
|
||||
image.Pngsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
break;
|
||||
case EncodeFormat.WEBP:
|
||||
image.Webpsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
break;
|
||||
case EncodeFormat.AVIF:
|
||||
image.Heifsave(Path.Combine(_directoryService.FaviconDirectory, filename));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(encodeFormat), encodeFormat, null);
|
||||
}
|
||||
|
||||
|
||||
_logger.LogDebug("Favicon.png for {Domain} downloaded and saved successfully", domain);
|
||||
return filename;
|
||||
|
|
@ -242,14 +274,13 @@ public class ImageService : IImageService
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName, bool saveAsWebP = false, int thumbnailWidth = ThumbnailWidth)
|
||||
public string CreateThumbnailFromBase64(string encodedImage, string fileName, EncodeFormat encodeFormat, int thumbnailWidth = ThumbnailWidth)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var thumbnail = Image.ThumbnailBuffer(Convert.FromBase64String(encodedImage), thumbnailWidth);
|
||||
fileName += (saveAsWebP ? ".webp" : ".png");
|
||||
fileName += encodeFormat.GetExtension();
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, fileName));
|
||||
return fileName;
|
||||
}
|
||||
|
|
@ -309,6 +340,7 @@ public class ImageService : IImageService
|
|||
/// <returns></returns>
|
||||
public static string GetReadingListFormat(int readingListId)
|
||||
{
|
||||
// ReSharper disable once StringLiteralTypo
|
||||
return $"readinglist{readingListId}";
|
||||
}
|
||||
|
||||
|
|
@ -322,9 +354,9 @@ public class ImageService : IImageService
|
|||
return $"thumbnail{chapterId}";
|
||||
}
|
||||
|
||||
public static string GetWebLinkFormat(string url)
|
||||
public static string GetWebLinkFormat(string url, EncodeFormat encodeFormat)
|
||||
{
|
||||
return $"{new Uri(url).Host}.png";
|
||||
return $"{new Uri(url).Host}{encodeFormat.GetExtension()}";
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
312
API/Services/MediaConversionService.cs
Normal file
312
API/Services/MediaConversionService.cs
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.SignalR;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public interface IMediaConversionService
|
||||
{
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllBookmarkToEncoding();
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllCoversToEncoding();
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
Task ConvertAllManagedMediaToEncodingFormat();
|
||||
|
||||
Task<string> SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder,
|
||||
EncodeFormat encodeFormat);
|
||||
}
|
||||
|
||||
public class MediaConversionService : IMediaConversionService
|
||||
{
|
||||
public const string Name = "MediaConversionService";
|
||||
public static readonly string[] ConversionMethods = {"ConvertAllBookmarkToEncoding", "ConvertAllCoversToEncoding", "ConvertAllManagedMediaToEncodingFormat"};
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IImageService _imageService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
private readonly ILogger<MediaConversionService> _logger;
|
||||
|
||||
public MediaConversionService(IUnitOfWork unitOfWork, IImageService imageService, IEventHub eventHub,
|
||||
IDirectoryService directoryService, ILogger<MediaConversionService> logger)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_imageService = imageService;
|
||||
_eventHub = eventHub;
|
||||
_directoryService = directoryService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts all Kavita managed media (bookmarks, covers, favicons, etc) to the saved target encoding.
|
||||
/// Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
/// <remarks>This is a long-running job</remarks>
|
||||
/// <returns></returns>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllManagedMediaToEncodingFormat()
|
||||
{
|
||||
await ConvertAllBookmarkToEncoding();
|
||||
await ConvertAllCoversToEncoding();
|
||||
await CoverAllFaviconsToEncoding();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a long-running job that will convert all bookmarks into a format that is not PNG. Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllBookmarkToEncoding()
|
||||
{
|
||||
var bookmarkDirectory =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (encodeFormat == EncodeFormat.PNG)
|
||||
{
|
||||
_logger.LogError("Cannot convert media to PNG");
|
||||
return;
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
|
||||
var bookmarks = (await _unitOfWork.UserRepository.GetAllBookmarksAsync())
|
||||
.Where(b => !b.FileName.EndsWith(encodeFormat.GetExtension())).ToList();
|
||||
|
||||
var count = 1F;
|
||||
foreach (var bookmark in bookmarks)
|
||||
{
|
||||
bookmark.FileName = await SaveAsEncodingFormat(bookmarkDirectory, bookmark.FileName,
|
||||
BookmarkService.BookmarkStem(bookmark.AppUserId, bookmark.SeriesId, bookmark.ChapterId), encodeFormat);
|
||||
_unitOfWork.UserRepository.Update(bookmark);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(count / bookmarks.Count, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Converted bookmarks to {Format}", encodeFormat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a long-running job that will convert all covers into WebP. Do not invoke anyway except via Hangfire.
|
||||
/// </summary>
|
||||
[DisableConcurrentExecution(timeoutInSeconds: 2 * 60 * 60), AutomaticRetry(Attempts = 0)]
|
||||
public async Task ConvertAllCoversToEncoding()
|
||||
{
|
||||
var coverDirectory = _directoryService.CoverImageDirectory;
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (encodeFormat == EncodeFormat.PNG)
|
||||
{
|
||||
_logger.LogError("Cannot convert media to PNG");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of all covers to {Format}", encodeFormat);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(0F, ProgressEventType.Started));
|
||||
|
||||
var chapterCovers = await _unitOfWork.ChapterRepository.GetAllChaptersWithCoversInDifferentEncoding(encodeFormat);
|
||||
var seriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
|
||||
|
||||
var readingListCovers = await _unitOfWork.ReadingListRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
|
||||
var libraryCovers = await _unitOfWork.LibraryRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
|
||||
var collectionCovers = await _unitOfWork.CollectionTagRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
|
||||
|
||||
var totalCount = chapterCovers.Count + seriesCovers.Count + readingListCovers.Count +
|
||||
libraryCovers.Count + collectionCovers.Count;
|
||||
|
||||
var count = 1F;
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of chapters");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(0, ProgressEventType.Started));
|
||||
foreach (var chapter in chapterCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapter.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsEncodingFormat(coverDirectory, chapter.CoverImage, coverDirectory, encodeFormat);
|
||||
chapter.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of series");
|
||||
foreach (var series in seriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsEncodingFormat(coverDirectory, series.CoverImage, coverDirectory, encodeFormat);
|
||||
series.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of libraries");
|
||||
foreach (var library in libraryCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(library.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsEncodingFormat(coverDirectory, library.CoverImage, coverDirectory, encodeFormat);
|
||||
library.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of reading lists");
|
||||
foreach (var readingList in readingListCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(readingList.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsEncodingFormat(coverDirectory, readingList.CoverImage, coverDirectory, encodeFormat);
|
||||
readingList.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.ReadingListRepository.Update(readingList);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of collections");
|
||||
foreach (var collection in collectionCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(collection.CoverImage)) continue;
|
||||
|
||||
var newFile = await SaveAsEncodingFormat(coverDirectory, collection.CoverImage, coverDirectory, encodeFormat);
|
||||
collection.CoverImage = Path.GetFileName(newFile);
|
||||
_unitOfWork.CollectionTagRepository.Update(collection);
|
||||
await _unitOfWork.CommitAsync();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(count / totalCount, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
// Now null out all series and volumes that aren't webp or custom
|
||||
var nonCustomOrConvertedVolumeCovers = await _unitOfWork.VolumeRepository.GetAllWithCoversInDifferentEncoding(encodeFormat);
|
||||
foreach (var volume in nonCustomOrConvertedVolumeCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(volume.CoverImage)) continue;
|
||||
volume.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.VolumeRepository.Update(volume);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
var nonCustomOrConvertedSeriesCovers = await _unitOfWork.SeriesRepository.GetAllWithCoversInDifferentEncoding(encodeFormat, false);
|
||||
foreach (var series in nonCustomOrConvertedSeriesCovers)
|
||||
{
|
||||
if (string.IsNullOrEmpty(series.CoverImage)) continue;
|
||||
series.CoverImage = null; // We null it out so when we call Refresh Metadata it will auto update from first chapter
|
||||
_unitOfWork.SeriesRepository.Update(series);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertCoverProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Converted covers to {Format}", encodeFormat);
|
||||
}
|
||||
|
||||
private async Task CoverAllFaviconsToEncoding()
|
||||
{
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (encodeFormat == EncodeFormat.PNG)
|
||||
{
|
||||
_logger.LogError("Cannot convert media to PNG");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Starting conversion of favicons to {Format}", encodeFormat);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(0F, ProgressEventType.Started));
|
||||
var pngFavicons = _directoryService.GetFiles(_directoryService.FaviconDirectory)
|
||||
.Where(b => !b.EndsWith(encodeFormat.GetExtension())).
|
||||
ToList();
|
||||
|
||||
var count = 1F;
|
||||
foreach (var file in pngFavicons)
|
||||
{
|
||||
await SaveAsEncodingFormat(_directoryService.FaviconDirectory, _directoryService.FileSystem.FileInfo.New(file).Name, _directoryService.FaviconDirectory,
|
||||
encodeFormat);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(count / pngFavicons.Count, ProgressEventType.Updated));
|
||||
count++;
|
||||
}
|
||||
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ConvertBookmarksProgressEvent(1F, ProgressEventType.Ended));
|
||||
|
||||
_logger.LogInformation("[MediaConversionService] Converted favicons to {Format}", encodeFormat);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts an image file, deletes original and returns the new path back
|
||||
/// </summary>
|
||||
/// <param name="imageDirectory">Full Path to where files are stored</param>
|
||||
/// <param name="filename">The file to convert</param>
|
||||
/// <param name="targetFolder">Full path to where files should be stored or any stem</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> SaveAsEncodingFormat(string imageDirectory, string filename, string targetFolder, EncodeFormat encodeFormat)
|
||||
{
|
||||
// This must be Public as it's used in via Hangfire as a background task
|
||||
var fullSourcePath = _directoryService.FileSystem.Path.Join(imageDirectory, filename);
|
||||
var fullTargetDirectory = fullSourcePath.Replace(new FileInfo(filename).Name, string.Empty);
|
||||
|
||||
var newFilename = string.Empty;
|
||||
_logger.LogDebug("Converting {Source} image into {Encoding} at {Target}", fullSourcePath, encodeFormat, fullTargetDirectory);
|
||||
|
||||
if (!File.Exists(fullSourcePath))
|
||||
{
|
||||
_logger.LogError("Requested to convert {File} but it doesn't exist", fullSourcePath);
|
||||
return newFilename;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Convert target file to format then delete original target file
|
||||
try
|
||||
{
|
||||
var targetFile = await _imageService.ConvertToEncodingFormat(fullSourcePath, fullTargetDirectory, encodeFormat);
|
||||
var targetName = new FileInfo(targetFile).Name;
|
||||
newFilename = Path.Join(targetFolder, targetName);
|
||||
_directoryService.DeleteFiles(new[] {fullSourcePath});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert image {FilePath} to {Format}", filename, encodeFormat);
|
||||
newFilename = filename;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not convert image to {Format}", encodeFormat);
|
||||
}
|
||||
|
||||
return newFilename;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using API.Comparators;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
|
|
@ -32,7 +33,7 @@ public interface IMetadataService
|
|||
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
|
||||
|
||||
Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true);
|
||||
Task GenerateCoversForSeries(Series series, bool convertToWebP, bool forceUpdate = false);
|
||||
Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false);
|
||||
Task RemoveAbandonedMetadataKeys();
|
||||
}
|
||||
|
||||
|
|
@ -63,8 +64,8 @@ public class MetadataService : IMetadataService
|
|||
/// </summary>
|
||||
/// <param name="chapter"></param>
|
||||
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
|
||||
/// <param name="convertToWebPOnWrite">Convert image to WebP when extracting the cover</param>
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, bool convertToWebPOnWrite)
|
||||
/// <param name="encodeFormat">Convert image to Encoding Format when extracting the cover</param>
|
||||
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat)
|
||||
{
|
||||
var firstFile = chapter.Files.MinBy(x => x.Chapter);
|
||||
if (firstFile == null) return Task.FromResult(false);
|
||||
|
|
@ -78,7 +79,7 @@ public class MetadataService : IMetadataService
|
|||
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
|
||||
|
||||
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath,
|
||||
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, convertToWebPOnWrite);
|
||||
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat);
|
||||
_unitOfWork.ChapterRepository.Update(chapter);
|
||||
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
|
||||
return Task.FromResult(true);
|
||||
|
|
@ -141,8 +142,8 @@ public class MetadataService : IMetadataService
|
|||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
/// <param name="convertToWebP"></param>
|
||||
private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, bool convertToWebP)
|
||||
/// <param name="encodeFormat"></param>
|
||||
private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat)
|
||||
{
|
||||
_logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName);
|
||||
try
|
||||
|
|
@ -155,7 +156,7 @@ public class MetadataService : IMetadataService
|
|||
var index = 0;
|
||||
foreach (var chapter in volume.Chapters)
|
||||
{
|
||||
var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, convertToWebP);
|
||||
var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat);
|
||||
// If cover was update, either the file has changed or first scan and we should force a metadata update
|
||||
UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated);
|
||||
if (index == 0 && chapterUpdated)
|
||||
|
|
@ -207,7 +208,7 @@ public class MetadataService : IMetadataService
|
|||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"));
|
||||
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++)
|
||||
{
|
||||
|
|
@ -237,7 +238,7 @@ public class MetadataService : IMetadataService
|
|||
|
||||
try
|
||||
{
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, convertToWebP);
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -287,23 +288,23 @@ public class MetadataService : IMetadataService
|
|||
return;
|
||||
}
|
||||
|
||||
var convertToWebP = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP;
|
||||
await GenerateCoversForSeries(series, convertToWebP, forceUpdate);
|
||||
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
await GenerateCoversForSeries(series, encodeFormat, forceUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate Cover for a Series. This is used by Scan Loop and should not be invoked directly via User Interaction.
|
||||
/// </summary>
|
||||
/// <param name="series">A full Series, with metadata, chapters, etc</param>
|
||||
/// <param name="convertToWebP">When saving the file, use WebP encoding instead of PNG</param>
|
||||
/// <param name="encodeFormat">When saving the file, what encoding should be used</param>
|
||||
/// <param name="forceUpdate"></param>
|
||||
public async Task GenerateCoversForSeries(Series series, bool convertToWebP, bool forceUpdate = false)
|
||||
public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name));
|
||||
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, convertToWebP);
|
||||
await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat);
|
||||
|
||||
|
||||
if (_unitOfWork.HasChanges())
|
||||
|
|
|
|||
|
|
@ -236,7 +236,6 @@ public class ReaderService : IReaderService
|
|||
|
||||
try
|
||||
{
|
||||
// TODO: Rewrite this code to just pull user object with progress for that particular appuserprogress, else create it
|
||||
var userProgress =
|
||||
await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(progressDto.ChapterId, userId);
|
||||
|
||||
|
|
@ -667,15 +666,15 @@ public class ReaderService : IReaderService
|
|||
_directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, ImageService.GetThumbnailFormat(chapter.Id));
|
||||
try
|
||||
{
|
||||
var saveAsWebp =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertBookmarkToWebP;
|
||||
var encodeFormat =
|
||||
(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
|
||||
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
{
|
||||
var outputtedThumbnails = cachedImages
|
||||
.Select((img, idx) =>
|
||||
_directoryService.FileSystem.Path.Join(outputDirectory,
|
||||
_imageService.WriteCoverThumbnail(img, $"{idx}", outputDirectory, saveAsWebp)))
|
||||
_imageService.WriteCoverThumbnail(img, $"{idx}", outputDirectory, encodeFormat)))
|
||||
.ToArray();
|
||||
return CacheService.GetPageFromFiles(outputtedThumbnails, pageNum);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ public interface IReadingItemService
|
|||
{
|
||||
ComicInfo? GetComicInfo(string filePath);
|
||||
int GetNumberOfPages(string filePath, MangaFormat format);
|
||||
string GetCoverImage(string filePath, string fileName, MangaFormat format, bool saveAsWebP);
|
||||
string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat);
|
||||
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
|
||||
ParserInfo? ParseFile(string path, string rootPath, LibraryType type);
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ public class ReadingItemService : IReadingItemService
|
|||
}
|
||||
}
|
||||
|
||||
public string GetCoverImage(string filePath, string fileName, MangaFormat format, bool saveAsWebP)
|
||||
public string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
|
|
@ -171,10 +171,10 @@ public class ReadingItemService : IReadingItemService
|
|||
|
||||
return format switch
|
||||
{
|
||||
MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, saveAsWebP),
|
||||
MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat),
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -336,7 +336,7 @@ public class ReadingListService : IReadingListService
|
|||
// .Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList();
|
||||
//
|
||||
// var combinedFile = ImageService.CreateMergedImage(fullImages, _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, $"{readingListId}.png"));
|
||||
// // webp needs to be handled
|
||||
// // webp/avif needs to be handled
|
||||
// return combinedFile;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public interface ITaskScheduler
|
|||
void CancelStatsTasks();
|
||||
Task RunStatCollection();
|
||||
void ScanSiteThemes();
|
||||
Task CovertAllCoversToWebP();
|
||||
Task CovertAllCoversToEncoding();
|
||||
Task CleanupDbEntries();
|
||||
|
||||
}
|
||||
|
|
@ -50,9 +50,9 @@ public class TaskScheduler : ITaskScheduler
|
|||
private readonly IThemeService _themeService;
|
||||
private readonly IWordCountAnalyzerService _wordCountAnalyzerService;
|
||||
private readonly IStatisticService _statisticService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IMediaConversionService _mediaConversionService;
|
||||
|
||||
public static BackgroundJobServer Client => new BackgroundJobServer();
|
||||
public static BackgroundJobServer Client => new ();
|
||||
public const string ScanQueue = "scan";
|
||||
public const string DefaultQueue = "default";
|
||||
public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read";
|
||||
|
|
@ -68,12 +68,17 @@ public class TaskScheduler : ITaskScheduler
|
|||
|
||||
private static readonly Random Rnd = new Random();
|
||||
|
||||
private static readonly RecurringJobOptions RecurringJobOptions = new RecurringJobOptions()
|
||||
{
|
||||
TimeZone = TimeZoneInfo.Local
|
||||
};
|
||||
|
||||
|
||||
public TaskScheduler(ICacheService cacheService, ILogger<TaskScheduler> logger, IScannerService scannerService,
|
||||
IUnitOfWork unitOfWork, IMetadataService metadataService, IBackupService backupService,
|
||||
ICleanupService cleanupService, IStatsService statsService, IVersionUpdaterService versionUpdaterService,
|
||||
IThemeService themeService, IWordCountAnalyzerService wordCountAnalyzerService, IStatisticService statisticService,
|
||||
IBookmarkService bookmarkService)
|
||||
IMediaConversionService mediaConversionService)
|
||||
{
|
||||
_cacheService = cacheService;
|
||||
_logger = logger;
|
||||
|
|
@ -87,7 +92,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
_themeService = themeService;
|
||||
_wordCountAnalyzerService = wordCountAnalyzerService;
|
||||
_statisticService = statisticService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_mediaConversionService = mediaConversionService;
|
||||
}
|
||||
|
||||
public async Task ScheduleTasks()
|
||||
|
|
@ -100,28 +105,28 @@ public class TaskScheduler : ITaskScheduler
|
|||
var scanLibrarySetting = setting;
|
||||
_logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting);
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => _scannerService.ScanLibraries(false),
|
||||
() => CronConverter.ConvertToCronNotation(scanLibrarySetting), TimeZoneInfo.Local);
|
||||
() => CronConverter.ConvertToCronNotation(scanLibrarySetting), RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false), Cron.Daily, RecurringJobOptions);
|
||||
}
|
||||
|
||||
setting = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.TaskBackup)).Value;
|
||||
if (setting != null)
|
||||
{
|
||||
_logger.LogDebug("Scheduling Backup Task for {Setting}", setting);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), Cron.Weekly, RecurringJobOptions);
|
||||
}
|
||||
|
||||
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(CleanupTaskId, () => _cleanupService.Cleanup(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(CleanupDbTaskId, () => _cleanupService.CleanupDbEntries(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(RemoveFromWantToReadTaskId, () => _cleanupService.CleanupWantToRead(), Cron.Daily, RecurringJobOptions);
|
||||
RecurringJob.AddOrUpdate(UpdateYearlyStatsTaskId, () => _statisticService.UpdateServerStatistics(), Cron.Monthly, RecurringJobOptions);
|
||||
}
|
||||
|
||||
#region StatsTasks
|
||||
|
|
@ -137,7 +142,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
}
|
||||
|
||||
_logger.LogDebug("Scheduling stat collection daily");
|
||||
RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate(ReportStatsTaskId, () => _statsService.Send(), Cron.Daily(Rnd.Next(0, 22)), RecurringJobOptions);
|
||||
}
|
||||
|
||||
public void AnalyzeFilesForLibrary(int libraryId, bool forceUpdate = false)
|
||||
|
|
@ -182,10 +187,20 @@ public class TaskScheduler : ITaskScheduler
|
|||
BackgroundJob.Enqueue(() => _themeService.Scan());
|
||||
}
|
||||
|
||||
public async Task CovertAllCoversToWebP()
|
||||
/// <summary>
|
||||
/// Do not invoke this manually, always enqueue on a background thread
|
||||
/// </summary>
|
||||
public async Task CovertAllCoversToEncoding()
|
||||
{
|
||||
await _bookmarkService.ConvertAllCoverToWebP();
|
||||
_logger.LogInformation("[BookmarkService] Queuing tasks to update Series and Volume references via Cover Refresh");
|
||||
var defaultParams = Array.Empty<object>();
|
||||
if (MediaConversionService.ConversionMethods.Any(method =>
|
||||
HasAlreadyEnqueuedTask(MediaConversionService.Name, method, defaultParams, DefaultQueue, true)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _mediaConversionService.ConvertAllManagedMediaToEncodingFormat();
|
||||
_logger.LogInformation("Queuing tasks to update Series and Volume references via Cover Refresh");
|
||||
var libraryIds = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var lib in libraryIds)
|
||||
{
|
||||
|
|
@ -200,8 +215,10 @@ public class TaskScheduler : ITaskScheduler
|
|||
public void ScheduleUpdaterTasks()
|
||||
{
|
||||
_logger.LogInformation("Scheduling Auto-Update tasks");
|
||||
// Schedule update check between noon and 6pm local time
|
||||
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(12, 18)), TimeZoneInfo.Local);
|
||||
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(5, 23)), new RecurringJobOptions()
|
||||
{
|
||||
TimeZone = TimeZoneInfo.Local
|
||||
});
|
||||
}
|
||||
|
||||
public void ScanFolder(string folderPath, TimeSpan delay)
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ public class CleanupService : ICleanupService
|
|||
[AutomaticRetry(Attempts = 3, LogEvents = false, OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||
public async Task Cleanup()
|
||||
{
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToWebP", Array.Empty<object>(),
|
||||
if (TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllCoverToEncoding", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true) ||
|
||||
TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToWebP", Array.Empty<object>(),
|
||||
TaskScheduler.HasAlreadyEnqueuedTask(BookmarkService.Name, "ConvertAllBookmarkToEncoding", Array.Empty<object>(),
|
||||
TaskScheduler.DefaultQueue, true))
|
||||
{
|
||||
_logger.LogInformation("Cleanup put on hold as a conversion to WebP in progress");
|
||||
_logger.LogInformation("Cleanup put on hold as a media conversion in progress");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a conversion to WebP in progress"));
|
||||
MessageFactory.ErrorEvent("Cleanup", "Cleanup put on hold as a media conversion in progress"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public static class Parser
|
|||
private const int RegexTimeoutMs = 5000000; // 500 ms
|
||||
public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif)";
|
||||
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)";
|
||||
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
|
||||
private const string BookFileExtensions = @"\.epub|\.pdf";
|
||||
private const string XmlRegexExtensions = @"\.xml";
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ public class ProcessSeries : IProcessSeries
|
|||
_logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name);
|
||||
}
|
||||
|
||||
await _metadataService.GenerateCoversForSeries(series, (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).ConvertCoverToWebP);
|
||||
await _metadataService.GenerateCoversForSeries(series, (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs);
|
||||
EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class StatsService : IStatsService
|
|||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly DataContext _context;
|
||||
private readonly IStatisticService _statisticService;
|
||||
private const string ApiUrl = "https://stats.kavitareader.com";
|
||||
private const string ApiUrl = "http://localhost:5003";
|
||||
|
||||
public StatsService(ILogger<StatsService> logger, IUnitOfWork unitOfWork, DataContext context, IStatisticService statisticService)
|
||||
{
|
||||
|
|
@ -139,8 +139,7 @@ public class StatsService : IStatsService
|
|||
TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(),
|
||||
TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(),
|
||||
UsingSeriesRelationships = await GetIfUsingSeriesRelationship(),
|
||||
StoreBookmarksAsWebP = serverSettings.ConvertBookmarkToWebP,
|
||||
StoreCoversAsWebP = serverSettings.ConvertCoverToWebP,
|
||||
EncodeMediaAs = serverSettings.EncodeMediaAs,
|
||||
MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(),
|
||||
MaxVolumesInASeries = await MaxVolumesInASeries(),
|
||||
MaxChaptersInASeries = await MaxChaptersInASeries(),
|
||||
|
|
@ -292,14 +291,14 @@ public class StatsService : IStatsService
|
|||
|
||||
private IEnumerable<FileFormatDto> AllFormats()
|
||||
{
|
||||
// TODO: Rewrite this with new migration code in feature/basic-stats
|
||||
|
||||
var results = _context.MangaFile
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.Select(m => new FileFormatDto()
|
||||
{
|
||||
Format = m.Format,
|
||||
Extension = Path.GetExtension(m.FilePath)?.ToLowerInvariant()!
|
||||
Extension = m.Extension
|
||||
})
|
||||
.DistinctBy(f => f.Extension)
|
||||
.ToList();
|
||||
|
|
|
|||
|
|
@ -113,13 +113,13 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
|
||||
if (BuildInfo.Version < updateVersion)
|
||||
{
|
||||
_logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
|
||||
_logger.LogWarning("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update),
|
||||
true);
|
||||
}
|
||||
else if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development)
|
||||
{
|
||||
_logger.LogInformation("Server is up to date. Current: {CurrentVersion}", BuildInfo.Version);
|
||||
_logger.LogWarning("Server is up to date. Current: {CurrentVersion}", BuildInfo.Version);
|
||||
await _eventHub.SendMessageAsync(MessageFactory.UpdateAvailable, MessageFactory.UpdateVersionEvent(update),
|
||||
true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ public class TokenService : ITokenService
|
|||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var tokenContent = tokenHandler.ReadJwtToken(request.Token);
|
||||
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.NameId)?.Value;
|
||||
var username = tokenContent.Claims.FirstOrDefault(q => q.Type == JwtRegisteredClaimNames.Name)?.Value;
|
||||
if (string.IsNullOrEmpty(username)) return null;
|
||||
var user = await _userManager.FindByIdAsync(username);
|
||||
var user = await _userManager.FindByNameAsync(username);
|
||||
if (user == null) return null; // This forces a logout
|
||||
var validated = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider, RefreshTokenName, request.RefreshToken);
|
||||
if (!validated) return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue