Implemented the ability to allow the admin to change the cover generation size. (#2213)

Changed how covers are merged together. Now a cover image will always be generated for reading list and collections.

Fixed reading list page being a bit laggy due to a missing trackby function.

Reading list page will now show the cover image always. Collection detail page will only hide the image if there is no summary on the collection.
This commit is contained in:
Joe Milazzo 2023-08-14 06:56:09 -05:00 committed by GitHub
parent 19801af6f3
commit d134196470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 221 additions and 87 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -158,11 +158,6 @@ public class ImageController : BaseApiController
private async Task<string> 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<string> 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;
}

View File

@ -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;

View File

@ -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
/// </summary>
public int OnDeckUpdateDays { get; set; }
/// <summary>
/// How large the cover images should be
/// </summary>
public CoverImageSize CoverImageSize { get; set; }
}

View File

@ -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<string>();
return data
.OrderBy(_ => random.Next())
.Take(4)

View File

@ -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<string>();
return data
.OrderBy(_ => random.Next())
.Take(4)

View File

@ -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

View File

@ -0,0 +1,36 @@
namespace API.Entities.Enums;
public enum CoverImageSize
{
/// <summary>
/// Default Size: 320x455 (wxh)
/// </summary>
Default = 1,
/// <summary>
/// 640x909
/// </summary>
Medium = 2,
/// <summary>
/// 900x1277
/// </summary>
Large = 3,
/// <summary>
/// 1265x1795
/// </summary>
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)
};
}
}

View File

@ -143,5 +143,10 @@ public enum ServerSettingKey
/// </summary>
[Description("OnDeckUpdateDays")]
OnDeckUpdateDays = 26,
/// <summary>
/// The size of the cover image thumbnail. Defaults to <see cref="CoverImageSize"/>.Default
/// </summary>
[Description("CoverImageSize")]
CoverImageSize = 27
}

View File

@ -79,6 +79,9 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
case ServerSettingKey.OnDeckUpdateDays:
destination.OnDeckUpdateDays = int.Parse(row.Value);
break;
case ServerSettingKey.CoverImageSize:
destination.CoverImageSize = Enum.Parse<CoverImageSize>(row.Value);
break;
}
}

View File

@ -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
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
/// <param name="format">When saving the file, use encoding</param>
/// <returns></returns>
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;

View File

@ -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);
/// <summary>
@ -1196,13 +1196,13 @@ public class BookService : IBookService
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
/// <param name="encodeFormat">When saving the file, use encoding</param>
/// <returns></returns>
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)

View File

@ -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);
/// <summary>
/// Creates a Thumbnail version of a base64 image
@ -40,7 +40,7 @@ public interface IImageService
/// <param name="outputDirectory"></param>
/// <param name="encodeFormat"></param>
/// <returns></returns>
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat);
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
/// <summary>
/// Writes out a thumbnail by file path input
/// </summary>
@ -49,7 +49,7 @@ public interface IImageService
/// <param name="outputDirectory"></param>
/// <param name="encodeFormat"></param>
/// <returns></returns>
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat);
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
/// <summary>
/// Converts the passed image to encoding and outputs it in the same directory
/// </summary>
@ -87,6 +87,7 @@ public class ImageService : IImageService
/// </summary>
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
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</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, 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<string> coverImages, string dest)
public static void CreateMergedImage(IList<string> 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;
}
}

View File

@ -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));
}

View File

@ -33,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, 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
/// <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="encodeFormat">Convert image to Encoding Format when extracting the cover</param>
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat)
private Task<bool> 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
/// <param name="series"></param>
/// <param name="forceUpdate"></param>
/// <param name="encodeFormat"></param>
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);
}
/// <summary>
@ -298,13 +302,13 @@ public class MetadataService : IMetadataService
/// <param name="series">A full Series, with metadata, chapters, etc</param>
/// <param name="encodeFormat">When saving the file, what encoding should be used</param>
/// <param name="forceUpdate"></param>
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())

View File

@ -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
};
}

View File

@ -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);
}

View File

@ -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'}
];

View File

@ -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;
}

View File

@ -7,7 +7,7 @@
<br/><b>{{t('encode-as-warning')}}</b>
</p>
<div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">{{t('media-warning')}}</div>
<div class="col-md-6 col-sm-12 mb-3">
<div class="col-md-6 col-sm-12 mb-3 pe-1">
<label for="settings-media-encodeMediaAs" class="form-label me-1">{{t('encode-as-label')}}</label>
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
<ng-template #encodeMediaAsTooltip>{{t('encode-as-tooltip')}}</ng-template>
@ -16,6 +16,16 @@
<option *ngFor="let format of EncodeFormats" [value]="format.value">{{format.title}}</option>
</select>
</div>
<div class="col-md-6 col-sm-12 mb-3">
<label for="settings-media-coverImageSize" class="form-label me-1">{{t('cover-image-size-label')}}</label>
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="coverImageSizeTooltip" role="button" tabindex="0"></i>
<ng-template #coverImageSizeTooltip>{{t('cover-image-size-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-media-coverImageSize-help"><ng-container [ngTemplateOutlet]="coverImageSizeTooltip"></ng-container></span>
<select class="form-select" aria-describedby="settings-media-coverImageSize-help" formControlName="coverImageSize" id="settings-media-coverImageSize">
<option *ngFor="let size of coverImageSizes" [value]="size.value">{{size.title}}</option>
</select>
</div>
</div>
<div class="row g-0">

View File

@ -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;

View File

@ -11,7 +11,9 @@
[trackByIdentity]="trackByIdentity"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [imageUrl]="imageSerivce.getCollectionCoverImage(item.id)" (clicked)="loadCollection(item)"></app-card-item>
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
[imageUrl]="imageSerivce.getCollectionCoverImage(item.id)"
(clicked)="loadCollection(item)"></app-card-item>
</ng-template>
<ng-template #noData>

View File

@ -11,9 +11,9 @@
</div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock>
<div class="row mb-3" *ngIf="(collectionTag.coverImage !== '' && collectionTag.coverImage !== undefined && collectionTag.coverImage !== null) || summary.length > 0">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="collectionTag.coverImage !== '' && collectionTag.coverImage !== undefined && collectionTag.coverImage !== null">
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image>
<div class="row mb-3" *ngIf="summary.length > 0">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image maxWidth="481px" [imageUrl]="imageService.getCollectionCoverImage(collectionTag.id)"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>

View File

@ -70,7 +70,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
translocoService = inject(TranslocoService);
collectionTag!: CollectionTag;
tagImage: string = '';
isLoading: boolean = true;
series: Array<Series> = [];
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, '<br>');
// 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();
}
});
}

View File

@ -38,8 +38,8 @@
<div class="container-fluid mt-2" *ngIf="readingList" >
<div class="row mb-2">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="readingList.coverImage !== '' && readingList.coverImage !== undefined && readingList.coverImage !== null">
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="readingListImage"></app-image>
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="imageService.getReadingListCoverImage(readingList.id)"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<div class="row g-0 mb-3">

View File

@ -62,7 +62,6 @@ export class ReadingListDetailComponent implements OnInit {
downloadInProgress: boolean = false;
readingListSummary: string = '';
readingListImage: string = '';
libraryTypes: {[key: number]: LibraryType} = {};
characters$!: Observable<Person[]>;
@ -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, '<br>');
this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
this.cdRef.markForCheck();
})
});

View File

@ -2,7 +2,7 @@
<app-side-nav-companion-bar>
<h2 title>
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
<span>Reading Lists</span>
<span>{{t('title')}}</span>
</h2>
<h6 subtitle class="subtitle-with-actionables" *ngIf="pagination">{{t('item-count', {num: pagination.totalItems | number})}}</h6>
</app-side-nav-companion-bar>
@ -13,6 +13,7 @@
[pagination]="pagination"
[jumpBarKeys]="jumpbarKeys"
[filteringDisabled]="true"
[trackByIdentity]="trackByIdentity"
>
<ng-template #cardItem let-item let-position="idx" >
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"

View File

@ -19,6 +19,7 @@ import { NgIf, DecimalPipe } from '@angular/common';
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {CollectionTag} from "../../../_models/collection-tag";
@Component({
selector: 'app-reading-lists',
@ -37,6 +38,7 @@ export class ReadingListsComponent implements OnInit {
jumpbarKeys: Array<JumpKey> = [];
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
globalActions: Array<ActionItem<any>> = [{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,

View File

@ -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": {

View File

@ -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