diff --git a/API/Constants.cs b/API/Constants.cs
new file mode 100644
index 0000000..253bf59
--- /dev/null
+++ b/API/Constants.cs
@@ -0,0 +1,18 @@
+using SixLabors.ImageSharp;
+
+namespace API;
+
+public struct Constants
+{
+ public const string TRANGA =
+ "\n\n" +
+ " _______ v2\n" +
+ "|_ _|.----..---.-..-----..-----..---.-.\n" +
+ " | | | _|| _ || || _ || _ |\n" +
+ " |___| |__| |___._||__|__||___ ||___._|\n" +
+ " |_____| \n\n";
+
+ public static readonly Size ImageSmSize = new (225, 320);
+ public static readonly Size ImageMdSize = new (450, 640);
+ public static readonly Size ImageLgSize = new (900, 1280);
+}
\ No newline at end of file
diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs
index 60f7443..3788a98 100644
--- a/API/Controllers/MangaController.cs
+++ b/API/Controllers/MangaController.cs
@@ -1,4 +1,5 @@
-using API.Controllers.DTOs;
+using System.Diagnostics.CodeAnalysis;
+using API.Controllers.DTOs;
using API.Schema.MangaContext;
using API.Workers;
using Asp.Versioning;
@@ -7,10 +8,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Net.Http.Headers;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats.Jpeg;
-using SixLabors.ImageSharp.Processing;
-using SixLabors.ImageSharp.Processing.Processors.Transforms;
using static Microsoft.AspNetCore.Http.StatusCodes;
using AltTitle = API.Controllers.DTOs.AltTitle;
using Author = API.Controllers.DTOs.Author;
@@ -190,54 +187,49 @@ public class MangaController(MangaContext context) : Controller
/// Returns Cover of with
///
/// .Key
- /// If is provided, needs to also be provided
- /// If is provided, needs to also be provided
+ /// Size of the cover returned
+ ///
-
+ ///
-
+ ///
-
+ ///
/// JPEG Image
/// Cover not loaded
- /// The formatting-request was invalid
/// with not found
/// Retry later, downloading cover
- [HttpGet("{MangaId}/Cover")]
- [ProducesResponseType(Status200OK,"image/jpeg")]
+ [HttpGet("{MangaId}/Cover/{CoverSize?}")]
+ [ProducesResponseType(Status200OK,"image/jpeg")]
[ProducesResponseType(Status204NoContent)]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound, "text/plain")]
[ProducesResponseType(Status503ServiceUnavailable)]
- public async Task, StatusCodeHttpResult>> GetCover (string MangaId, [FromQuery]int? width, [FromQuery]int? height)
+ public async Task, StatusCodeHttpResult>> GetCover (string MangaId, CoverSize? CoverSize = null)
{
if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga)
return TypedResults.NotFound(nameof(MangaId));
-
- if (!System.IO.File.Exists(manga.CoverFileNameInCache))
+
+ string cache = CoverSize switch
+ {
+ MangaController.CoverSize.Small => TrangaSettings.coverImageCacheSmall,
+ MangaController.CoverSize.Medium => TrangaSettings.coverImageCacheMedium,
+ MangaController.CoverSize.Large => TrangaSettings.coverImageCacheLarge,
+ _ => TrangaSettings.coverImageCacheOriginal
+ };
+
+ if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data)
{
if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId))
{
Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}");
return TypedResults.StatusCode(Status503ServiceUnavailable);
}
- else
- return TypedResults.NoContent();
- }
-
- Image image = await Image.LoadAsync(manga.CoverFileNameInCache, HttpContext.RequestAborted);
-
- if (width is { } w && height is { } h)
- {
- if (width < 10 || height < 10 || width > 65535 || height > 65535)
- return TypedResults.BadRequest();
- image.Mutate(i => i.ApplyProcessor(new ResizeProcessor(new ResizeOptions()
- {
- Mode = ResizeMode.Max,
- Size = new Size(w, h)
- }, image.Size)));
+ return TypedResults.NoContent();
}
- using MemoryStream ms = new();
- await image.SaveAsync(ms, new JpegEncoder(){Quality = 100}, HttpContext.RequestAborted);
- DateTime lastModified = new FileInfo(manga.CoverFileNameInCache).LastWriteTime;
+ DateTime lastModified = data.fileInfo.LastWriteTime;
HttpContext.Response.Headers.CacheControl = "public";
- return TypedResults.File(ms.GetBuffer(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
+ return TypedResults.Bytes(data.stream.ToArray(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""));
}
+ public enum CoverSize { Original, Large, Medium, Small }
///
/// Returns all of with
diff --git a/API/MangaConnectors/MangaConnector.cs b/API/MangaConnectors/MangaConnector.cs
index d02718a..ef72924 100644
--- a/API/MangaConnectors/MangaConnector.cs
+++ b/API/MangaConnectors/MangaConnector.cs
@@ -6,6 +6,9 @@ using API.Schema.MangaContext;
using log4net;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Processing;
namespace API.MangaConnectors;
@@ -43,8 +46,8 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
Regex urlRex = new (@"https?:\/\/((?:[a-zA-Z0-9-]+\.)+[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+))");
//https?:\/\/[a-zA-Z0-9-]+\.([a-zA-Z0-9-]+\.[a-zA-Z0-9]+)\/(?:.+\/)*(.+\.([a-zA-Z]+)) for only second level domains
Match match = urlRex.Match(mangaId.Obj.CoverUrl);
- string filename = $"{match.Groups[1].Value}-{mangaId.Key}.{match.Groups[3].Value}";
- string saveImagePath = Path.Join(TrangaSettings.coverImageCache, filename);
+ string filename = $"{match.Groups[1].Value}-{mangaId.ObjId}.{mangaId.MangaConnectorName}.{match.Groups[3].Value}";
+ string saveImagePath = Path.Join(TrangaSettings.coverImageCacheOriginal, filename);
if (File.Exists(saveImagePath))
return saveImagePath;
@@ -53,11 +56,36 @@ public abstract class MangaConnector(string name, string[] supportedLanguages, s
if ((int)coverResult.statusCode < 200 || (int)coverResult.statusCode >= 300)
return SaveCoverImageToCache(mangaId, --retries);
- using MemoryStream ms = new();
- coverResult.result.CopyTo(ms);
- Directory.CreateDirectory(TrangaSettings.coverImageCache);
- File.WriteAllBytes(saveImagePath, ms.ToArray());
-
- return saveImagePath;
+ try
+ {
+ using MemoryStream ms = new();
+ coverResult.result.CopyTo(ms);
+ byte[] imageBytes = ms.ToArray();
+ Directory.CreateDirectory(TrangaSettings.coverImageCacheOriginal);
+ File.WriteAllBytes(saveImagePath, imageBytes);
+
+ using Image image = Image.Load(imageBytes);
+ Directory.CreateDirectory(TrangaSettings.coverImageCacheLarge);
+ using Image large = image.Clone(x => x.Resize(new ResizeOptions
+ { Size = Constants.ImageLgSize, Mode = ResizeMode.Max }));
+ large.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheLarge, filename), new (){ Quality = 40 });
+
+ Directory.CreateDirectory(TrangaSettings.coverImageCacheMedium);
+ using Image medium = image.Clone(x => x.Resize(new ResizeOptions
+ { Size = Constants.ImageMdSize, Mode = ResizeMode.Max }));
+ medium.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheMedium, filename), new (){ Quality = 40 });
+
+ Directory.CreateDirectory(TrangaSettings.coverImageCacheSmall);
+ using Image small = image.Clone(x => x.Resize(new ResizeOptions
+ { Size = Constants.ImageSmSize, Mode = ResizeMode.Max }));
+ small.SaveAsJpeg(Path.Join(TrangaSettings.coverImageCacheSmall, filename), new (){ Quality = 40 });
+ }
+ catch (Exception e)
+ {
+ Log.Error(e);
+ }
+
+
+ return filename;
}
}
\ No newline at end of file
diff --git a/API/Schema/MangaContext/Manga.cs b/API/Schema/MangaContext/Manga.cs
index 66b25c0..9c4bd1a 100644
--- a/API/Schema/MangaContext/Manga.cs
+++ b/API/Schema/MangaContext/Manga.cs
@@ -4,6 +4,10 @@ using System.Runtime.InteropServices;
using System.Text;
using API.Workers;
using Microsoft.EntityFrameworkCore;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
using static System.IO.UnixFileMode;
namespace API.Schema.MangaContext;
@@ -155,6 +159,18 @@ public class Manga : Identifiable
return newJobs.ToArray();
}
+ public async Task<(MemoryStream stream, FileInfo fileInfo)?> GetCoverImage(string cachePath, CancellationToken ct)
+ {
+ string fullPath = Path.Join(cachePath, CoverFileNameInCache);
+ if (!File.Exists(fullPath))
+ return null;
+
+ FileInfo fileInfo = new(fullPath);
+ MemoryStream stream = new (await File.ReadAllBytesAsync(fullPath, ct));
+
+ return (stream, fileInfo);
+ }
+
public override string ToString() => $"{base.ToString()} {Name}";
}
diff --git a/API/Tranga.cs b/API/Tranga.cs
index a5cf3d8..a6dc836 100644
--- a/API/Tranga.cs
+++ b/API/Tranga.cs
@@ -15,16 +15,6 @@ namespace API;
public static class Tranga
{
-
- // ReSharper disable once InconsistentNaming
- private const string TRANGA =
- "\n\n" +
- " _______ v2\n" +
- "|_ _|.----..---.-..-----..-----..---.-.\n" +
- " | | | _|| _ || || _ || _ |\n" +
- " |___| |__| |___._||__|__||___ ||___._|\n" +
- " |_____| \n\n";
-
private static IServiceProvider? ServiceProvider;
private static readonly ILog Log = LogManager.GetLogger(typeof(Tranga));
@@ -45,7 +35,7 @@ public static class Tranga
{
XmlConfigurator.ConfigureAndWatch(loggerConfigFile);
Log.Info("Logger Configured.");
- Log.Info(TRANGA);
+ Log.Info(Constants.TRANGA);
}
internal static void AddDefaultWorkers()
diff --git a/API/TrangaSettings.cs b/API/TrangaSettings.cs
index 7cd24d6..f10ec75 100644
--- a/API/TrangaSettings.cs
+++ b/API/TrangaSettings.cs
@@ -11,8 +11,11 @@ public struct TrangaSettings()
public static string workingDirectory => Path.Join(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/usr/share" : Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "tranga-api");
[JsonIgnore]
public static string settingsFilePath => Path.Join(workingDirectory, "settings.json");
- [JsonIgnore]
- public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
+ [JsonIgnore] public static string coverImageCache => Path.Join(workingDirectory, "imageCache");
+ [JsonIgnore] public static string coverImageCacheOriginal => Path.Join(coverImageCache, "original");
+ [JsonIgnore] public static string coverImageCacheLarge => Path.Join(coverImageCache, "large");
+ [JsonIgnore] public static string coverImageCacheMedium => Path.Join(coverImageCache, "medium");
+ [JsonIgnore] public static string coverImageCacheSmall => Path.Join(coverImageCache, "small");
public string DownloadLocation => RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "/Manga" : Path.Join(Directory.GetCurrentDirectory(), "Manga");
[JsonIgnore]
internal static readonly string DefaultUserAgent = $"Tranga/2.0 ({Enum.GetName(Environment.OSVersion.Platform)}; {(Environment.Is64BitOperatingSystem ? "x64" : "")})";
diff --git a/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs b/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs
index 94ecbf9..4fbe244 100644
--- a/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs
+++ b/API/Workers/MangaDownloadWorkers/DownloadChapterFromMangaconnectorWorker.cs
@@ -200,18 +200,19 @@ public class DownloadChapterFromMangaconnectorWorker(MangaConnectorId c
Log.Info($"Copying cover to {publicationFolder}");
await DbContext.Entry(mangaConnectorId).Navigation(nameof(MangaConnectorId.Obj)).LoadAsync(CancellationToken);
- string? fileInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
- if (fileInCache is null)
+ string? coverFileNameInCache = manga.CoverFileNameInCache ?? mangaConnector.SaveCoverImageToCache(mangaConnectorId);
+ if (coverFileNameInCache is null)
{
- Log.Error($"File {fileInCache} does not exist");
+ Log.Error($"File {coverFileNameInCache} does not exist");
return;
}
- string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(fileInCache).Split('.')[^1]}" );
- File.Copy(fileInCache, newFilePath, true);
+ string fullCoverPath = Path.Join(TrangaSettings.coverImageCacheOriginal, coverFileNameInCache);
+ string newFilePath = Path.Join(publicationFolder, $"cover.{Path.GetFileName(coverFileNameInCache).Split('.')[^1]}" );
+ File.Copy(fullCoverPath, newFilePath, true);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
File.SetUnixFileMode(newFilePath, GroupRead | GroupWrite | UserRead | UserWrite | OtherRead | OtherWrite);
- Log.Debug($"Copied cover from {fileInCache} to {newFilePath}");
+ Log.Debug($"Copied cover from {fullCoverPath} to {newFilePath}");
}
private bool DownloadImage(string imageUrl, string savePath)
diff --git a/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs b/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs
index c30a96a..d462d23 100644
--- a/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs
+++ b/API/Workers/PeriodicWorkers/MaintenanceWorkers/CleanupMangaCoversWorker.cs
@@ -11,11 +11,22 @@ public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable DoWorkInternal()
{
Log.Info("Removing stale files...");
- if (!Directory.Exists(TrangaSettings.coverImageCache))
- return new Task(() => []);
string[] usedFiles = DbContext.Mangas.Select(m => m.CoverFileNameInCache).Where(s => s != null).ToArray()!;
- string[] extraneousFiles = new DirectoryInfo(TrangaSettings.coverImageCache).GetFiles()
- .Where(f => usedFiles.Contains(f.FullName) == false)
+ CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheOriginal);
+ CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheLarge);
+ CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheMedium);
+ CleanupImageCache(usedFiles, TrangaSettings.coverImageCacheSmall);
+ return new Task(() => []);
+ }
+
+ private void CleanupImageCache(string[] retainFilenames, string imageCachePath)
+ {
+ DirectoryInfo directory = new(imageCachePath);
+ if (!directory.Exists)
+ return;
+ string[] extraneousFiles = directory
+ .GetFiles()
+ .Where(f => retainFilenames.Contains(f.Name) == false)
.Select(f => f.FullName)
.ToArray();
foreach (string path in extraneousFiles)
@@ -23,6 +34,5 @@ public class CleanupMangaCoversWorker(TimeSpan? interval = null, IEnumerable(() => []);
}
}
\ No newline at end of file