Files
MilkyShots/Lactose/Jobs/ThumbnailJob.cs
REDCODE be1e02bcf2 Fixed Race condition on Thumbnail job signaling.
Moved compose to use proper docker configs instead of normal development configs.
2025-09-05 00:48:18 +02:00

201 lines
6.8 KiB
C#

using Butter.Settings;
using Lactose.Models;
using Lactose.Repositories;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
namespace Lactose.Jobs;
public class ThumbnailJob : Job {
ThumbnailJob? ParentJob;
List<ThumbnailJob>? subJobs;
Asset? Asset;
string? ThumbnailPath;
int processedAssets = 0;
int totalAssets = 1; // Avoid division by zero
int ThumbnailSize;
ILogger<ThumbnailJob> Logger;
ISettingsRepository SettingsRepository;
IAssetRepository AssetRepository;
JobManager? JobManager;
public override string Name { get; }
public ThumbnailJob(
ILogger<ThumbnailJob> logger,
ISettingsRepository settingsRepository,
IAssetRepository assetRepository,
JobManager jobManager
) {
Logger = logger;
SettingsRepository = settingsRepository;
AssetRepository = assetRepository;
JobManager = jobManager;
Name = "Thumbnail Generation Job";
}
public ThumbnailJob(
ThumbnailJob parentJob,
Asset asset,
int thumbnailSize,
string thumbnailPath,
ILogger<ThumbnailJob> logger,
ISettingsRepository settingsRepository,
IAssetRepository assetRepository
) {
Logger = logger;
SettingsRepository = settingsRepository;
AssetRepository = assetRepository;
Asset = asset;
Name = $"Thumbnail Gen for {Asset?.OriginalPath}";
ParentJob = parentJob;
ThumbnailSize = thumbnailSize;
ThumbnailPath = thumbnailPath;
}
protected override async Task TaskJob(CancellationToken token) {
if (ParentJob == null) MasterJob(token);
else SlaveJob(token);
}
void SlaveJob(CancellationToken token) {
if (token.IsCancellationRequested) {
JobStatus.Cancel("Cancellation requested from user.");
return;
}
if (Asset == null) {
Logger.LogError("Sub-job started without an asset. Canceling job.");
JobStatus.Fail("Sub-job started without an asset. Canceling job.");
return;
}
JobStatus.Start();
try {
if (string.IsNullOrEmpty(Asset.OriginalPath) || !File.Exists(Asset.OriginalPath)) {
var msg = $"Original file for asset ID {Asset.Id} not found at path {Asset.OriginalPath}";
Logger.LogWarning(msg);
JobStatus.Fail(msg);
return;
}
using var image = Image.Load(Asset.OriginalPath);
image.Mutate(x => x.Resize(
new ResizeOptions {
Size = new Size(ThumbnailSize),
Mode = ResizeMode.Max,
Sampler = KnownResamplers.CatmullRom
}
)
);
if (ThumbnailPath == null) {
var msg = "Thumbnail path is not set. Cannot save thumbnail.";
Logger.LogError(msg);
JobStatus.Fail(msg);
return;
}
var path = PathFromGuid(Asset.Id, ThumbnailPath);
if (!Directory.Exists(path)) Directory.CreateDirectory(Path.GetDirectoryName(path)!);
image.SaveAsJpeg(
path,
new JpegEncoder() {
ColorType = JpegEncodingColor.Rgb,
Quality = 75,
SkipMetadata = true
}
);
Asset.ThumbnailPath = path;
AssetRepository.Update(Asset);
AssetRepository.Save();
} catch (Exception ex) {
Logger.LogError(ex, $"Failed to generate thumbnail for asset ID {Asset.Id}");
JobStatus.Fail($"Error: {ex.Message}");
return;
}
Logger.LogInformation($"Thumbnail generated for asset ID {Asset.Id} at {Asset.ThumbnailPath}");
JobStatus.Complete($"Thumbnail generated for asset ID {Asset.Id} at {Asset.ThumbnailPath}");
}
void MasterJob(CancellationToken token) {
if (JobManager == null) {
Logger.LogError("JobManager is not available. Cannot create sub-jobs.");
JobStatus.Fail("JobManager is not available. Cannot create sub-jobs.");
return;
}
JobStatus.Start();
Logger.LogInformation("Starting master Thumbnail job for all assets.");
var thumbPath = SettingsRepository.Get(Settings.ThumbnailPath.AsString());
//TODO: Add thumbnail size setting to the system
if (thumbPath == null) {
Logger.LogError("Thumbnail path not found. Cannot proceed with thumbnail generation.");
JobStatus.Fail("Thumbnail path not found. Cannot proceed with thumbnail generation.");
return;
}
var missingThumbnail = AssetRepository.GetAssetsMissingThumbnail(out totalAssets);
Logger.LogInformation($"Found {totalAssets} assets missing thumbnails.");
subJobs = new List<ThumbnailJob>();
foreach (var asset in missingThumbnail) {
if (token.IsCancellationRequested) {
lock (subJobs) subJobs.ForEach(j => j.Cancel());
JobStatus.Cancel("Cancellation requested from user.");
return;
}
// Create a sub-job for each asset
var job = JobManager.CreateJob<ThumbnailJob>(this, asset, 256, thumbPath.Value!);
job.Done += (o, _) => {
IncrementProcessedAssets();
lock (subJobs) subJobs.Remove((o as ThumbnailJob)!);
};
lock (subJobs) subJobs.Add(job);
JobManager.EnqueueJob(job);
}
JobStatus.Wait();
// Wait for all sub-jobs to complete
while (subJobs.Count > 0) {
if (token.IsCancellationRequested) {
lock (subJobs) subJobs.ForEach(j => j.Cancel());
JobStatus.Cancel("User requested cancellation.");
return;
}
}
JobStatus.Complete("All thumbnails generated successfully.");
}
void IncrementProcessedAssets() {
processedAssets++;
JobStatus.UpdateProgress((float)processedAssets / totalAssets, $"Waiting for {subJobs.Count} sub-jobs to complete.");
}
static string PathFromGuid(Guid id, string root) {
var s = id.ToString("N"); // 32 chars, no dashes
return Path.Combine(
root,
s[..2], // 3F
s.Substring(2, 2), // 25
s.Substring(4, 2), // 04
$"{s}.jpg"
);
}
}