Better OPDS Reading Lists & Cover Generation for Webtoons (#3017)
Co-authored-by: Zackaree <github@zackaree.com>
This commit is contained in:
parent
2fb72ab0d4
commit
a063333f80
24 changed files with 329 additions and 50 deletions
|
|
@ -125,14 +125,77 @@ public class ImageService : IImageService
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to determine if there is a better mode for resizing
|
||||
/// </summary>
|
||||
/// <param name="image"></param>
|
||||
/// <param name="targetWidth"></param>
|
||||
/// <param name="targetHeight"></param>
|
||||
/// <returns></returns>
|
||||
public static Enums.Size GetSizeForDimensions(Image image, int targetWidth, int targetHeight)
|
||||
{
|
||||
if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height))
|
||||
{
|
||||
return Enums.Size.Force;
|
||||
}
|
||||
|
||||
return Enums.Size.Both;
|
||||
}
|
||||
|
||||
public static Enums.Interesting? GetCropForDimensions(Image image, int targetWidth, int targetHeight)
|
||||
{
|
||||
|
||||
if (WillScaleWell(image, targetWidth, targetHeight) || IsLikelyWideImage(image.Width, image.Height))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Enums.Interesting.Attention;
|
||||
}
|
||||
|
||||
public static bool WillScaleWell(Image sourceImage, int targetWidth, int targetHeight, double tolerance = 0.1)
|
||||
{
|
||||
// Calculate the aspect ratios
|
||||
var sourceAspectRatio = (double) sourceImage.Width / sourceImage.Height;
|
||||
var targetAspectRatio = (double) targetWidth / targetHeight;
|
||||
|
||||
// Compare aspect ratios
|
||||
if (Math.Abs(sourceAspectRatio - targetAspectRatio) > tolerance)
|
||||
{
|
||||
return false; // Aspect ratios differ significantly
|
||||
}
|
||||
|
||||
// Calculate scaling factors
|
||||
var widthScaleFactor = (double)targetWidth / sourceImage.Width;
|
||||
var heightScaleFactor = (double)targetHeight / sourceImage.Height;
|
||||
|
||||
// Check resolution quality (example thresholds)
|
||||
if (widthScaleFactor > 2.0 || heightScaleFactor > 2.0)
|
||||
{
|
||||
return false; // Scaling factor too large
|
||||
}
|
||||
|
||||
return true; // Image will scale well
|
||||
}
|
||||
|
||||
private static bool IsLikelyWideImage(int width, int height)
|
||||
{
|
||||
var aspectRatio = (double) width / height;
|
||||
return aspectRatio > 1.25;
|
||||
}
|
||||
|
||||
public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var dims = size.GetDimensions();
|
||||
using var thumbnail = Image.Thumbnail(path, dims.Width, height: dims.Height, size: Enums.Size.Force);
|
||||
var (width, height) = size.GetDimensions();
|
||||
using var sourceImage = Image.NewFromFile(path, false, Enums.Access.SequentialUnbuffered);
|
||||
|
||||
using var thumbnail = Image.Thumbnail(path, width, height: height,
|
||||
size: GetSizeForDimensions(sourceImage, width, height),
|
||||
crop: GetCropForDimensions(sourceImage, width, height));
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
|
||||
return filename;
|
||||
|
|
@ -156,8 +219,14 @@ public class ImageService : IImageService
|
|||
/// <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, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
|
||||
{
|
||||
var dims = size.GetDimensions();
|
||||
using var thumbnail = Image.ThumbnailStream(stream, dims.Width, height: dims.Height, size: Enums.Size.Force);
|
||||
var (width, height) = size.GetDimensions();
|
||||
stream.Position = 0;
|
||||
using var sourceImage = Image.NewFromStream(stream);
|
||||
stream.Position = 0;
|
||||
|
||||
using var thumbnail = Image.ThumbnailStream(stream, width, height: height,
|
||||
size: GetSizeForDimensions(sourceImage, width, height),
|
||||
crop: GetCropForDimensions(sourceImage, width, height));
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
|
|
@ -170,8 +239,12 @@ public class ImageService : IImageService
|
|||
|
||||
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
|
||||
{
|
||||
var dims = size.GetDimensions();
|
||||
using var thumbnail = Image.Thumbnail(sourceFile, dims.Width, height: dims.Height, size: Enums.Size.Force);
|
||||
var (width, height) = size.GetDimensions();
|
||||
using var sourceImage = Image.NewFromFile(sourceFile, false, Enums.Access.SequentialUnbuffered);
|
||||
|
||||
using var thumbnail = Image.Thumbnail(sourceFile, width, height: height,
|
||||
size: GetSizeForDimensions(sourceImage, width, height),
|
||||
crop: GetCropForDimensions(sourceImage, width, height));
|
||||
var filename = fileName + encodeFormat.GetExtension();
|
||||
_directoryService.ExistOrCreate(outputDirectory);
|
||||
try
|
||||
|
|
@ -426,7 +499,7 @@ public class ImageService : IImageService
|
|||
|
||||
public static void CreateMergedImage(IList<string> coverImages, CoverImageSize size, string dest)
|
||||
{
|
||||
var dims = size.GetDimensions();
|
||||
var (width, height) = size.GetDimensions();
|
||||
int rows, cols;
|
||||
|
||||
if (coverImages.Count == 1)
|
||||
|
|
@ -446,7 +519,7 @@ public class ImageService : IImageService
|
|||
}
|
||||
|
||||
|
||||
var image = Image.Black(dims.Width, dims.Height);
|
||||
var image = Image.Black(width, height);
|
||||
|
||||
var thumbnailWidth = image.Width / cols;
|
||||
var thumbnailHeight = image.Height / rows;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
public const string KavitaPlusDataRefreshId = "kavita+-data-refresh";
|
||||
public const string KavitaPlusStackSyncId = "kavita+-stack-sync";
|
||||
|
||||
private static readonly ImmutableArray<string> ScanTasks =
|
||||
public static readonly ImmutableArray<string> ScanTasks =
|
||||
["ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"];
|
||||
|
||||
private static readonly Random Rnd = new Random();
|
||||
|
|
@ -123,7 +123,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
{
|
||||
var scanLibrarySetting = setting;
|
||||
_logger.LogDebug("Scheduling Scan Library Task for {Setting}", scanLibrarySetting);
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => _scannerService.ScanLibraries(false),
|
||||
RecurringJob.AddOrUpdate(ScanLibrariesTaskId, () => ScanLibraries(false),
|
||||
() => CronConverter.ConvertToCronNotation(scanLibrarySetting), RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
|
|
@ -345,6 +345,9 @@ public class TaskScheduler : ITaskScheduler
|
|||
return;
|
||||
}
|
||||
|
||||
// await _eventHub.SendMessageAsync(MessageFactory.Info,
|
||||
// MessageFactory.InfoEvent($"Scan library invoked but a task is already running for {library.Name}. Rescheduling request for 10 mins", string.Empty));
|
||||
|
||||
_logger.LogInformation("Enqueuing library scan for: {LibraryId}", libraryId);
|
||||
BackgroundJob.Enqueue(() => _scannerService.ScanLibrary(libraryId, force, true));
|
||||
// When we do a scan, force cache to re-unpack in case page numbers change
|
||||
|
|
@ -463,6 +466,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
HasAlreadyEnqueuedTask(ScannerService.Name, "ScanSeries", [seriesId, false], ScanQueue, checkRunningJobs);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this same invocation is already enqueued or scheduled
|
||||
/// </summary>
|
||||
|
|
@ -471,6 +475,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
/// <param name="args">object[] of arguments in the order they are passed to enqueued job</param>
|
||||
/// <param name="queue">Queue to check against. Defaults to "default"</param>
|
||||
/// <param name="checkRunningJobs">Check against running jobs. Defaults to false.</param>
|
||||
/// <param name="checkArgs">Check against arguments. Defaults to true.</param>
|
||||
/// <returns></returns>
|
||||
public static bool HasAlreadyEnqueuedTask(string className, string methodName, object[] args, string queue = DefaultQueue, bool checkRunningJobs = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ public enum ScanCancelReason
|
|||
public class ScannerService : IScannerService
|
||||
{
|
||||
public const string Name = "ScannerService";
|
||||
public const int Timeout = 60 * 60 * 60;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ILogger<ScannerService> _logger;
|
||||
private readonly IMetadataService _metadataService;
|
||||
|
|
@ -168,6 +169,7 @@ public class ScannerService : IScannerService
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
// This is basically rework of what's already done in Library Watcher but is needed if invoked via API
|
||||
var parentDirectory = _directoryService.GetParentDirectoryName(folder);
|
||||
if (string.IsNullOrEmpty(parentDirectory)) return;
|
||||
|
|
@ -202,6 +204,14 @@ public class ScannerService : IScannerService
|
|||
|
||||
var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId);
|
||||
if (series == null) return; // This can occur when UI deletes a series but doesn't update and user re-requests update
|
||||
|
||||
// if (TaskScheduler.HasScanTaskRunningForSeries(seriesId))
|
||||
// {
|
||||
// _logger.LogInformation("[ScannerService] Scan series invoked but a task is already running/enqueued. Rescheduling request for 1 mins");
|
||||
// BackgroundJob.Schedule(() => ScanSeries(seriesId, bypassFolderOptimizationChecks), TimeSpan.FromMinutes(1));
|
||||
// return;
|
||||
// }
|
||||
|
||||
var existingChapterIdsToClean = await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new[] {seriesId});
|
||||
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(series.LibraryId, LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns);
|
||||
|
|
@ -465,13 +475,22 @@ public class ScannerService : IScannerService
|
|||
}
|
||||
|
||||
[Queue(TaskScheduler.ScanQueue)]
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[DisableConcurrentExecution(Timeout)]
|
||||
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibraries(bool forceUpdate = false)
|
||||
{
|
||||
_logger.LogInformation("Starting Scan of All Libraries, Forced: {Forced}", forceUpdate);
|
||||
foreach (var lib in await _unitOfWork.LibraryRepository.GetLibrariesAsync())
|
||||
{
|
||||
if (TaskScheduler.RunningAnyTasksByMethod(TaskScheduler.ScanTasks, TaskScheduler.ScanQueue))
|
||||
{
|
||||
_logger.LogInformation("[ScannerService] Scan library invoked via nightly scan job but a task is already running. Rescheduling for 4 hours");
|
||||
await _eventHub.SendMessageAsync(MessageFactory.Info, MessageFactory.InfoEvent($"Scan libraries task delayed",
|
||||
$"A scan was ongoing during processing of the scan libraries task. Task has been rescheduled for {DateTime.UtcNow.AddHours(4)} UTC"));
|
||||
BackgroundJob.Schedule(() => ScanLibraries(forceUpdate), TimeSpan.FromHours(4));
|
||||
return;
|
||||
}
|
||||
|
||||
await ScanLibrary(lib.Id, forceUpdate, true);
|
||||
}
|
||||
_processSeries.Reset();
|
||||
|
|
@ -488,13 +507,14 @@ public class ScannerService : IScannerService
|
|||
/// <param name="forceUpdate">Defaults to false</param>
|
||||
/// <param name="isSingleScan">Defaults to true. Is this a standalone invocation or is it in a loop?</param>
|
||||
[Queue(TaskScheduler.ScanQueue)]
|
||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||
[DisableConcurrentExecution(Timeout)]
|
||||
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibrary(int libraryId, bool forceUpdate = false, bool isSingleScan = true)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId,
|
||||
LibraryIncludes.Folders | LibraryIncludes.FileTypes | LibraryIncludes.ExcludePatterns);
|
||||
|
||||
var libraryFolderPaths = library!.Folders.Select(fp => fp.Path).ToList();
|
||||
if (!await CheckMounts(library.Name, libraryFolderPaths)) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -91,12 +91,39 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
|
||||
// Find the latest dto
|
||||
var latestRelease = updateDtos[0]!;
|
||||
var updateVersion = new Version(latestRelease.UpdateVersion);
|
||||
var isNightly = BuildInfo.Version > new Version(latestRelease.UpdateVersion);
|
||||
|
||||
// isNightly can be true when we compare something like v0.8.1 vs v0.8.1.0
|
||||
if (IsVersionEqualToBuildVersion(updateVersion))
|
||||
{
|
||||
//latestRelease.UpdateVersion = BuildInfo.Version.ToString();
|
||||
isNightly = false;
|
||||
}
|
||||
|
||||
|
||||
latestRelease.IsOnNightlyInRelease = isNightly;
|
||||
|
||||
return updateDtos;
|
||||
}
|
||||
|
||||
private static bool IsVersionEqualToBuildVersion(Version updateVersion)
|
||||
{
|
||||
return updateVersion.Revision < 0 && BuildInfo.Version.Revision == 0 &&
|
||||
CompareWithoutRevision(BuildInfo.Version, updateVersion);
|
||||
}
|
||||
|
||||
private static bool CompareWithoutRevision(Version v1, Version v2)
|
||||
{
|
||||
if (v1.Major != v2.Major)
|
||||
return v1.Major == v2.Major;
|
||||
if (v1.Minor != v2.Minor)
|
||||
return v1.Minor == v2.Minor;
|
||||
if (v1.Build != v2.Build)
|
||||
return v1.Build == v2.Build;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> GetNumberOfReleasesBehind()
|
||||
{
|
||||
var updates = await GetAllReleases();
|
||||
|
|
@ -109,6 +136,7 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
var updateVersion = new Version(update.Tag_Name.Replace("v", string.Empty));
|
||||
var currentVersion = BuildInfo.Version.ToString(4);
|
||||
|
||||
|
||||
return new UpdateNotificationDto()
|
||||
{
|
||||
CurrentVersion = currentVersion,
|
||||
|
|
@ -118,7 +146,7 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
UpdateUrl = update.Html_Url,
|
||||
IsDocker = OsInfo.IsDocker,
|
||||
PublishDate = update.Published_At,
|
||||
IsReleaseEqual = BuildInfo.Version == updateVersion,
|
||||
IsReleaseEqual = IsVersionEqualToBuildVersion(updateVersion),
|
||||
IsReleaseNewer = BuildInfo.Version < updateVersion,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue