diff --git a/API.Tests/Services/CacheServiceTests.cs b/API.Tests/Services/CacheServiceTests.cs
index f59afcf74..e1419e052 100644
--- a/API.Tests/Services/CacheServiceTests.cs
+++ b/API.Tests/Services/CacheServiceTests.cs
@@ -42,7 +42,7 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
return 1;
}
- public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat)
+ public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{
return string.Empty;
}
diff --git a/API.Tests/Services/ParseScannedFilesTests.cs b/API.Tests/Services/ParseScannedFilesTests.cs
index 915e584ca..32ad8f645 100644
--- a/API.Tests/Services/ParseScannedFilesTests.cs
+++ b/API.Tests/Services/ParseScannedFilesTests.cs
@@ -43,7 +43,7 @@ internal class MockReadingItemService : IReadingItemService
return 1;
}
- public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat)
+ public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{
return string.Empty;
}
diff --git a/API/Controllers/ImageController.cs b/API/Controllers/ImageController.cs
index bbb32e042..8dcd3749d 100644
--- a/API/Controllers/ImageController.cs
+++ b/API/Controllers/ImageController.cs
@@ -158,11 +158,6 @@ public class ImageController : BaseApiController
private async Task GenerateReadingListCoverImage(int readingListId)
{
var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId);
- if (covers.Count < 4)
- {
- return string.Empty;
- }
-
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
ImageService.GetReadingListFormat(readingListId));
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
@@ -171,6 +166,7 @@ public class ImageController : BaseApiController
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
ImageService.CreateMergedImage(
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
+ settings.CoverImageSize,
destFile);
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
}
@@ -178,11 +174,6 @@ public class ImageController : BaseApiController
private async Task GenerateCollectionCoverImage(int collectionId)
{
var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId);
- if (covers.Count < 4)
- {
- return string.Empty;
- }
-
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
ImageService.GetCollectionTagFormat(collectionId));
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
@@ -190,6 +181,7 @@ public class ImageController : BaseApiController
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
ImageService.CreateMergedImage(
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
+ settings.CoverImageSize,
destFile);
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
}
diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs
index f1849a7ff..895a0b5c3 100644
--- a/API/Controllers/SettingsController.cs
+++ b/API/Controllers/SettingsController.cs
@@ -197,6 +197,12 @@ public class SettingsController : BaseApiController
_unitOfWork.SettingsRepository.Update(setting);
}
+ if (setting.Key == ServerSettingKey.CoverImageSize && updateSettingsDto.CoverImageSize + string.Empty != setting.Value)
+ {
+ setting.Value = updateSettingsDto.CoverImageSize + string.Empty;
+ _unitOfWork.SettingsRepository.Update(setting);
+ }
+
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
{
setting.Value = updateSettingsDto.TaskScan;
diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs
index 15dd9177b..e405758bc 100644
--- a/API/DTOs/Settings/ServerSettingDTO.cs
+++ b/API/DTOs/Settings/ServerSettingDTO.cs
@@ -84,4 +84,8 @@ public class ServerSettingDto
/// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically
///
public int OnDeckUpdateDays { get; set; }
+ ///
+ /// How large the cover images should be
+ ///
+ public CoverImageSize CoverImageSize { get; set; }
}
diff --git a/API/Data/Repositories/CollectionTagRepository.cs b/API/Data/Repositories/CollectionTagRepository.cs
index 5bc490e35..4e35ef613 100644
--- a/API/Data/Repositories/CollectionTagRepository.cs
+++ b/API/Data/Repositories/CollectionTagRepository.cs
@@ -127,7 +127,6 @@ public class CollectionTagRepository : ICollectionTagRepository
.Select(sm => sm.Series.CoverImage)
.Where(t => !string.IsNullOrEmpty(t))
.ToListAsync();
- if (data.Count < 4) return new List();
return data
.OrderBy(_ => random.Next())
.Take(4)
diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs
index bd0a5bafc..9c3d40011 100644
--- a/API/Data/Repositories/ReadingListRepository.cs
+++ b/API/Data/Repositories/ReadingListRepository.cs
@@ -101,7 +101,6 @@ public class ReadingListRepository : IReadingListRepository
.SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage))
.Where(t => !string.IsNullOrEmpty(t))
.ToListAsync();
- if (data.Count < 4) return new List();
return data
.OrderBy(_ => random.Next())
.Take(4)
diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs
index 076f086cd..a1941d17f 100644
--- a/API/Data/Seed.cs
+++ b/API/Data/Seed.cs
@@ -108,8 +108,9 @@ public static class Seed
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()},
new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty},
- new() {Key = ServerSettingKey.OnDeckProgressDays, Value = $"{30}"},
- new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = $"{7}"},
+ new() {Key = ServerSettingKey.OnDeckProgressDays, Value = "30"},
+ new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = "7"},
+ new() {Key = ServerSettingKey.CoverImageSize, Value = CoverImageSize.Default.ToString()},
new() {
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
}, // Not used from DB, but DB is sync with appSettings.json
diff --git a/API/Entities/Enums/CoverImageSize.cs b/API/Entities/Enums/CoverImageSize.cs
new file mode 100644
index 000000000..d2d0eebb6
--- /dev/null
+++ b/API/Entities/Enums/CoverImageSize.cs
@@ -0,0 +1,36 @@
+namespace API.Entities.Enums;
+
+public enum CoverImageSize
+{
+ ///
+ /// Default Size: 320x455 (wxh)
+ ///
+ Default = 1,
+ ///
+ /// 640x909
+ ///
+ Medium = 2,
+ ///
+ /// 900x1277
+ ///
+ Large = 3,
+ ///
+ /// 1265x1795
+ ///
+ XLarge = 4
+}
+
+public static class CoverImageSizeExtensions
+{
+ public static (int Width, int Height) GetDimensions(this CoverImageSize size)
+ {
+ return size switch
+ {
+ CoverImageSize.Default => (320, 455),
+ CoverImageSize.Medium => (640, 909),
+ CoverImageSize.Large => (900, 1277),
+ CoverImageSize.XLarge => (1265, 1795),
+ _ => (320, 455)
+ };
+ }
+}
diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs
index c8d9c12be..af699a3d9 100644
--- a/API/Entities/Enums/ServerSettingKey.cs
+++ b/API/Entities/Enums/ServerSettingKey.cs
@@ -143,5 +143,10 @@ public enum ServerSettingKey
///
[Description("OnDeckUpdateDays")]
OnDeckUpdateDays = 26,
+ ///
+ /// The size of the cover image thumbnail. Defaults to .Default
+ ///
+ [Description("CoverImageSize")]
+ CoverImageSize = 27
}
diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs
index 9163a027f..a55e104a7 100644
--- a/API/Helpers/Converters/ServerSettingConverter.cs
+++ b/API/Helpers/Converters/ServerSettingConverter.cs
@@ -79,6 +79,9 @@ public class ServerSettingConverter : ITypeConverter,
case ServerSettingKey.OnDeckUpdateDays:
destination.OnDeckUpdateDays = int.Parse(row.Value);
break;
+ case ServerSettingKey.CoverImageSize:
+ destination.CoverImageSize = Enum.Parse(row.Value);
+ break;
}
}
diff --git a/API/Services/ArchiveService.cs b/API/Services/ArchiveService.cs
index a4a9d3ccb..fd4349c90 100644
--- a/API/Services/ArchiveService.cs
+++ b/API/Services/ArchiveService.cs
@@ -22,7 +22,7 @@ public interface IArchiveService
{
void ExtractArchive(string archivePath, string extractPath);
int GetNumberOfPagesFromArchive(string archivePath);
- string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format);
+ string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default);
bool IsValidArchive(string archivePath);
ComicInfo? GetComicInfo(string archivePath);
ArchiveLibrary CanOpen(string archivePath);
@@ -205,7 +205,7 @@ public class ArchiveService : IArchiveService
/// Where to output the file, defaults to covers directory
/// When saving the file, use encoding
///
- public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format)
+ public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default)
{
if (archivePath == null || !IsValidArchive(archivePath)) return string.Empty;
try
@@ -221,7 +221,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, format);
+ return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size);
}
case ArchiveLibrary.SharpCompress:
{
@@ -232,7 +232,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, format);
+ return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size);
}
case ArchiveLibrary.NotSupported:
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
@@ -246,7 +246,7 @@ public class ArchiveService : IArchiveService
{
_logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath);
_mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService,
- "This archive cannot be read or not supported", ex);
+ "This archive cannot be read or not supported", ex); // TODO: Localize this
}
return string.Empty;
diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs
index 54d2257ab..06424c1a3 100644
--- a/API/Services/BookService.cs
+++ b/API/Services/BookService.cs
@@ -33,7 +33,7 @@ namespace API.Services;
public interface IBookService
{
int GetNumberOfPages(string filePath);
- string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat);
+ string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
ComicInfo? GetComicInfo(string filePath);
ParserInfo? ParseInfo(string filePath);
///
@@ -1196,13 +1196,13 @@ public class BookService : IBookService
/// Where to output the file, defaults to covers directory
/// When saving the file, use encoding
///
- public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
+ public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{
if (!IsValidFile(fileFilePath)) return string.Empty;
if (Parser.IsPdf(fileFilePath))
{
- return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat);
+ return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat, size);
}
using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions);
@@ -1217,20 +1217,20 @@ public class BookService : IBookService
if (coverImageContent == null) return string.Empty;
using var stream = coverImageContent.GetContentStream();
- return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
+ return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath);
_mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService,
- "There was a critical error and prevented thumbnail generation", ex);
+ "There was a critical error and prevented thumbnail generation", ex); // TODO: Localize this
}
return string.Empty;
}
- private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat)
+ private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size)
{
try
{
@@ -1240,7 +1240,7 @@ public class BookService : IBookService
using var stream = StreamManager.GetStream("BookService.GetPdfPage");
GetPdfPage(docReader, 0, stream);
- return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat);
+ return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size);
}
catch (Exception ex)
diff --git a/API/Services/ImageService.cs b/API/Services/ImageService.cs
index 54ea5ec38..6996eff43 100644
--- a/API/Services/ImageService.cs
+++ b/API/Services/ImageService.cs
@@ -21,7 +21,7 @@ namespace API.Services;
public interface IImageService
{
void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1);
- string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat);
+ string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size);
///
/// Creates a Thumbnail version of a base64 image
@@ -40,7 +40,7 @@ public interface IImageService
///
///
///
- string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat);
+ string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
///
/// Writes out a thumbnail by file path input
///
@@ -49,7 +49,7 @@ public interface IImageService
///
///
///
- string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat);
+ string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
///
/// Converts the passed image to encoding and outputs it in the same directory
///
@@ -87,6 +87,7 @@ public class ImageService : IImageService
///
public const int LibraryThumbnailWidth = 32;
+
private static readonly string[] ValidIconRelations = {
"icon",
"apple-touch-icon",
@@ -124,13 +125,14 @@ public class ImageService : IImageService
}
}
- public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat)
+ public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size)
{
if (string.IsNullOrEmpty(path)) return string.Empty;
try
{
- using var thumbnail = Image.Thumbnail(path, ThumbnailWidth, height: ThumbnailHeight, size: Enums.Size.Force);
+ var dims = size.GetDimensions();
+ using var thumbnail = Image.Thumbnail(path, dims.Width, height: dims.Height, size: Enums.Size.Force);
var filename = fileName + encodeFormat.GetExtension();
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
return filename;
@@ -152,9 +154,10 @@ public class ImageService : IImageService
/// Where to output the file, defaults to covers directory
/// Export the file as the passed encoding
/// File name with extension of the file. This will always write to
- public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat)
+ public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{
- using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth, height: ThumbnailHeight, size: Enums.Size.Force);
+ var dims = size.GetDimensions();
+ using var thumbnail = Image.ThumbnailStream(stream, dims.Width, height: dims.Height, size: Enums.Size.Force);
var filename = fileName + encodeFormat.GetExtension();
_directoryService.ExistOrCreate(outputDirectory);
try
@@ -165,9 +168,10 @@ public class ImageService : IImageService
return filename;
}
- public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat)
+ public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{
- using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth, height: ThumbnailHeight, size: Enums.Size.Force);
+ var dims = size.GetDimensions();
+ using var thumbnail = Image.Thumbnail(sourceFile, dims.Width, height: dims.Height, size: Enums.Size.Force);
var filename = fileName + encodeFormat.GetExtension();
_directoryService.ExistOrCreate(outputDirectory);
try
@@ -420,27 +424,58 @@ public class ImageService : IImageService
}
- public static string CreateMergedImage(IList coverImages, string dest)
+ public static void CreateMergedImage(IList coverImages, CoverImageSize size, string dest)
{
- var image = Image.Black(ThumbnailWidth, ThumbnailHeight); // 320x455
+ var dims = size.GetDimensions();
+ int rows, cols;
- var thumbnailWidth = image.Width / 2;
- var thumbnailHeight = image.Height / 2;
+ if (coverImages.Count == 1)
+ {
+ rows = 1;
+ cols = 1;
+ }
+ else if (coverImages.Count == 2)
+ {
+ rows = 1;
+ cols = 2;
+ }
+ else if (coverImages.Count == 3)
+ {
+ rows = 2;
+ cols = 2;
+ }
+ else
+ {
+ // Default to 2x2 layout for more than 3 images
+ rows = 2;
+ cols = 2;
+ }
+
+ var image = Image.Black(dims.Width, dims.Height);
+
+ var thumbnailWidth = image.Width / cols;
+ var thumbnailHeight = image.Height / rows;
for (var i = 0; i < coverImages.Count; i++)
{
var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential);
-
- // Resize the tile to fit the thumbnail size
tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight);
- var x = (i % 2) * thumbnailWidth;
- var y = (i / 2) * thumbnailHeight;
+ var row = i / cols;
+ var col = i % cols;
+
+ var x = col * thumbnailWidth;
+ var y = row * thumbnailHeight;
+
+ if (coverImages.Count == 3 && i == 2)
+ {
+ x = (image.Width - thumbnailWidth) / 2;
+ y = thumbnailHeight;
+ }
image = image.Insert(tile, x, y);
}
image.WriteToFile(dest);
- return dest;
}
}
diff --git a/API/Services/MediaErrorService.cs b/API/Services/MediaErrorService.cs
index 6615dab7a..30c51e61d 100644
--- a/API/Services/MediaErrorService.cs
+++ b/API/Services/MediaErrorService.cs
@@ -37,6 +37,7 @@ public class MediaErrorService : IMediaErrorService
public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex)
{
+ // TODO: Localize all these messages
// To avoid overhead on commits, do async. We don't need to wait.
BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message));
}
diff --git a/API/Services/MetadataService.cs b/API/Services/MetadataService.cs
index ff5a18df2..f6fb06063 100644
--- a/API/Services/MetadataService.cs
+++ b/API/Services/MetadataService.cs
@@ -33,7 +33,7 @@ public interface IMetadataService
/// Overrides any cache logic and forces execution
Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true);
- Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false);
+ Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false);
Task RemoveAbandonedMetadataKeys();
}
@@ -65,7 +65,7 @@ public class MetadataService : IMetadataService
///
/// Force updating cover image even if underlying file has not been modified or chapter already has a cover image
/// Convert image to Encoding Format when extracting the cover
- private Task UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat)
+ private Task UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
{
var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (firstFile == null) return Task.FromResult(false);
@@ -79,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, encodeFormat);
+ ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize);
_unitOfWork.ChapterRepository.Update(chapter);
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
return Task.FromResult(true);
@@ -143,7 +143,7 @@ public class MetadataService : IMetadataService
///
///
///
- private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat)
+ private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
{
_logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName);
try
@@ -156,7 +156,7 @@ public class MetadataService : IMetadataService
var index = 0;
foreach (var chapter in volume.Chapters)
{
- var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat);
+ var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize);
// 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)
@@ -208,7 +208,9 @@ public class MetadataService : IMetadataService
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"));
- var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
+ var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ var encodeFormat = settings.EncodeMediaAs;
+ var coverImageSize = settings.CoverImageSize;
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++)
{
@@ -238,7 +240,7 @@ public class MetadataService : IMetadataService
try
{
- await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat);
+ await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize);
}
catch (Exception ex)
{
@@ -288,8 +290,10 @@ public class MetadataService : IMetadataService
return;
}
- var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs;
- await GenerateCoversForSeries(series, encodeFormat, forceUpdate);
+ var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ var encodeFormat = settings.EncodeMediaAs;
+ var coverImageSize = settings.CoverImageSize;
+ await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate);
}
///
@@ -298,13 +302,13 @@ public class MetadataService : IMetadataService
/// A full Series, with metadata, chapters, etc
/// When saving the file, what encoding should be used
///
- public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false)
+ public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, 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, encodeFormat);
+ await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize);
if (_unitOfWork.HasChanges())
diff --git a/API/Services/ReadingItemService.cs b/API/Services/ReadingItemService.cs
index c163d45f0..86deed393 100644
--- a/API/Services/ReadingItemService.cs
+++ b/API/Services/ReadingItemService.cs
@@ -10,7 +10,7 @@ public interface IReadingItemService
{
ComicInfo? GetComicInfo(string filePath);
int GetNumberOfPages(string filePath, MangaFormat format);
- string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat);
+ string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
ParserInfo? ParseFile(string path, string rootPath, LibraryType type);
}
@@ -162,7 +162,7 @@ public class ReadingItemService : IReadingItemService
}
}
- public string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat)
+ public string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{
if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(fileName))
{
@@ -172,10 +172,10 @@ public class ReadingItemService : IReadingItemService
return format switch
{
- 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),
+ MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
+ MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
+ MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
+ MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
_ => string.Empty
};
}
diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs
index f30da8a47..55dba51b5 100644
--- a/API/Services/Tasks/Scanner/ProcessSeries.cs
+++ b/API/Services/Tasks/Scanner/ProcessSeries.cs
@@ -231,7 +231,8 @@ 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()).EncodeMediaAs);
+ var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
+ await _metadataService.GenerateCoversForSeries(series, settings.EncodeMediaAs, settings.CoverImageSize);
EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id);
}
diff --git a/UI/Web/src/app/admin/_models/cover-image-size.ts b/UI/Web/src/app/admin/_models/cover-image-size.ts
new file mode 100644
index 000000000..6a58de8ba
--- /dev/null
+++ b/UI/Web/src/app/admin/_models/cover-image-size.ts
@@ -0,0 +1,14 @@
+export enum CoverImageSize {
+ Default = 1,
+ Medium = 2,
+ Large = 3,
+ XLarge = 4
+}
+
+export const CoverImageSizes =
+ [
+ {value: CoverImageSize.Default, title: 'cover-image-size.default'},
+ {value: CoverImageSize.Medium, title: 'cover-image-size.medium'},
+ {value: CoverImageSize.Large, title: 'cover-image-size.large'},
+ {value: CoverImageSize.XLarge, title: 'cover-image-size.xlarge'}
+ ];
diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts
index b72396a8c..e58aa5190 100644
--- a/UI/Web/src/app/admin/_models/server-settings.ts
+++ b/UI/Web/src/app/admin/_models/server-settings.ts
@@ -1,4 +1,5 @@
import { EncodeFormat } from "./encode-format";
+import {CoverImageSize} from "./cover-image-size";
export interface ServerSettings {
cacheDirectory: string;
@@ -20,4 +21,5 @@ export interface ServerSettings {
cacheSize: number;
onDeckProgressDays: number;
onDeckUpdateDays: number;
+ coverImageSize: CoverImageSize;
}
diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html
index 7d78644d3..4b01b5b11 100644
--- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html
+++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.html
@@ -7,7 +7,7 @@
{{t('encode-as-warning')}}
{{t('media-warning')}}
-
+
{{t('encode-as-tooltip')}}
@@ -16,6 +16,16 @@
+
+
+
+
+ {{t('cover-image-size-tooltip')}}
+
+
+
diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts
index 62ff9094b..f1de0ed37 100644
--- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts
+++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts
@@ -21,7 +21,8 @@ import {EncodeFormats} from '../_models/encode-format';
import {ManageScrobbleErrorsComponent} from '../manage-scrobble-errors/manage-scrobble-errors.component';
import {ManageAlertsComponent} from '../manage-alerts/manage-alerts.component';
import {NgFor, NgIf, NgTemplateOutlet} from '@angular/common';
-import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
+import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
+import { CoverImageSizes } from '../_models/cover-image-size';
@Component({
selector: 'app-manage-media-settings',
@@ -38,6 +39,11 @@ export class ManageMediaSettingsComponent implements OnInit {
alertCount: number = 0;
scrobbleCount: number = 0;
+ coverImageSizes = CoverImageSizes.map(o => {
+ const newObj = {...o};
+ newObj.title = translate(o.title);
+ return newObj;
+ })
private readonly translocoService = inject(TranslocoService);
private readonly cdRef = inject(ChangeDetectorRef);
@@ -51,6 +57,7 @@ export class ManageMediaSettingsComponent implements OnInit {
this.serverSettings = settings;
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, [Validators.required]));
this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required]));
+ this.settingsForm.addControl('coverImageSize', new FormControl(this.serverSettings.coverImageSize, [Validators.required]));
this.cdRef.markForCheck();
});
}
@@ -58,6 +65,7 @@ export class ManageMediaSettingsComponent implements OnInit {
resetForm() {
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory);
+ this.settingsForm.get('coverImageSize')?.setValue(this.serverSettings.coverImageSize);
this.settingsForm.markAsPristine();
this.cdRef.markForCheck();
}
@@ -66,6 +74,7 @@ export class ManageMediaSettingsComponent implements OnInit {
const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10);
modelSettings.bookmarksDirectory = this.settingsForm.get('bookmarksDirectory')?.value;
+ modelSettings.coverImageSize = parseInt(this.settingsForm.get('coverImageSize')?.value, 10);
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.serverSettings = settings;
diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html
index ab0de261d..365120ac3 100644
--- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html
+++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html
@@ -11,7 +11,9 @@
[trackByIdentity]="trackByIdentity"
>
-
+
diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html
index 75fd01fb8..fd5c5d564 100644
--- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html
+++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html
@@ -11,9 +11,9 @@
-
0">
-
-
+
0">
+
diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts
index 10a1224ff..cade37ff9 100644
--- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts
+++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts
@@ -70,7 +70,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
translocoService = inject(TranslocoService);
collectionTag!: CollectionTag;
- tagImage: string = '';
isLoading: boolean = true;
series: Array
= [];
pagination!: Pagination;
@@ -229,9 +228,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
this.collectionTag = matchingTags[0];
this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '
');
- // TODO: This can be changed now that we have app-image and collection cover merge code (can it? Because we still have the case where there is no image)
- // I can always tweak merge to allow blank slots and if just one item, just show that item image
- this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
this.titleService.setTitle(this.translocoService.translate('collection-detail.title-alt', {collectionName: this.collectionTag.title}));
this.cdRef.markForCheck();
});
@@ -285,11 +281,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => {
this.updateTag(this.collectionTag.id);
this.loadPage();
- if (results.coverImageUpdated) {
- this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
- this.collectionTag.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
- this.cdRef.markForCheck();
- }
});
}
diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html
index c9552e8c6..52af401e7 100644
--- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html
+++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html
@@ -38,8 +38,8 @@
-
-
+
diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts
index a637e00df..24924139d 100644
--- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts
+++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts
@@ -62,7 +62,6 @@ export class ReadingListDetailComponent implements OnInit {
downloadInProgress: boolean = false;
readingListSummary: string = '';
- readingListImage: string = '';
libraryTypes: {[key: number]: LibraryType} = {};
characters$!: Observable
;
@@ -90,7 +89,6 @@ export class ReadingListDetailComponent implements OnInit {
}
this.listId = parseInt(listId, 10);
this.characters$ = this.readingListService.getCharacters(this.listId);
- this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
forkJoin([
this.libraryService.getLibraries(),
@@ -162,7 +160,6 @@ export class ReadingListDetailComponent implements OnInit {
this.readingListService.getReadingList(this.listId).subscribe(rl => {
this.readingList = rl;
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '
');
- this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
this.cdRef.markForCheck();
})
});
diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html
index 812fc44e6..eb82370d5 100644
--- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html
+++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.html
@@ -2,7 +2,7 @@
- Reading Lists
+ {{t('title')}}
{{t('item-count', {num: pagination.totalItems | number})}}
@@ -13,6 +13,7 @@
[pagination]="pagination"
[jumpBarKeys]="jumpbarKeys"
[filteringDisabled]="true"
+ [trackByIdentity]="trackByIdentity"
>
= [];
actions: {[key: number]: Array>} = {};
globalActions: Array> = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
+ trackByIdentity = (index: number, item: ReadingList) => `${item.id}_${item.title}`;
translocoService = inject(TranslocoService);
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json
index ba5a5952c..39adb5fff 100644
--- a/UI/Web/src/assets/langs/en.json
+++ b/UI/Web/src/assets/langs/en.json
@@ -1069,7 +1069,16 @@
"save": "{{common.save}}",
"media-issue-title": "Media Issues",
- "scrobble-issue-title": "Scrobble Issues"
+ "scrobble-issue-title": "Scrobble Issues",
+ "cover-image-size-label": "Cover Image Size",
+ "cover-image-size-tooltip": "How large should cover images generate as. Note: Anything larger than the default will incur longer page load times."
+ },
+
+ "cover-image-size": {
+ "default": "Default (320x455)",
+ "medium": "Medium (640x909)",
+ "large": "Large (900x1277)",
+ "xlarge": "Extra Large (1265x1795)"
},
"manage-scrobble-errors": {
diff --git a/openapi.json b/openapi.json
index 1cf089711..c14895509 100644
--- a/openapi.json
+++ b/openapi.json
@@ -7,7 +7,7 @@
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
- "version": "0.7.7.7"
+ "version": "0.7.7.9"
},
"servers": [
{
@@ -16658,6 +16658,17 @@
"type": "integer",
"description": "How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically",
"format": "int32"
+ },
+ "coverImageSize": {
+ "enum": [
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "type": "integer",
+ "description": "How large the cover images should be",
+ "format": "int32"
}
},
"additionalProperties": false