refactor(server): decouple generated images from image formats (#8246)

* rename

thumbnail config

update target paths, fix tests

rename to image settings

replace legacy enum

better typing

update sql

update api

remove config option

fix

* update docs

* update other thumbnail configs in migration

* keep legacy enum for now

* fix jumbled job names

* fix jumbled job names in tests

* rename thumbhash job

* rename dto

* fix tests

* preserve order

* remove unused import

* keep old fields in dto, marked deprecated

* update sql

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Mert
2024-04-02 00:56:56 -04:00
committed by GitHub
parent e520c0d1f5
commit 8edc2fb46f
66 changed files with 916 additions and 547 deletions

View File

@@ -1,5 +1,5 @@
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
@@ -7,6 +7,7 @@ import { AssetPathType } from 'src/entities/move.entity';
import {
AudioCodec,
Colorspace,
ImageFormat,
TranscodeHWAccel,
TranscodePolicy,
TranscodeTarget,
@@ -81,15 +82,15 @@ export class MediaService {
const jobs: JobItem[] = [];
for (const asset of assets) {
if (!asset.resizePath || force) {
jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: asset.id } });
if (!asset.previewPath || force) {
jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } });
continue;
}
if (!asset.webpPath) {
jobs.push({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { id: asset.id } });
if (!asset.thumbnailPath) {
jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } });
}
if (!asset.thumbhash) {
jobs.push({ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: { id: asset.id } });
jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } });
}
}
@@ -152,41 +153,41 @@ export class MediaService {
}
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
const { image } = await this.configCore.getConfig();
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) {
return JobStatus.FAILED;
}
await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL);
await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL);
await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO);
await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat);
await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
await this.storageCore.moveAssetVideo(asset);
return JobStatus.SUCCESS;
}
async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise<JobStatus> {
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset) {
return JobStatus.FAILED;
}
const resizePath = await this.generateThumbnail(asset, 'jpeg');
await this.assetRepository.update({ id: asset.id, resizePath });
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG);
await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS;
}
private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') {
const { thumbnail, ffmpeg } = await this.configCore.getConfig();
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize;
const path =
format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset);
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
const { image, ffmpeg } = await this.configCore.getConfig();
const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
const path = StorageCore.getImagePath(asset, type, format);
this.storageCore.ensureFolders(path);
switch (asset.type) {
case AssetType.IMAGE: {
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace;
const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality };
await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions);
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
const imageOptions = { format, size, colorspace, quality: image.quality };
await this.mediaRepository.resize(asset.originalPath, path, imageOptions);
break;
}
@@ -214,24 +215,24 @@ export class MediaService {
return path;
}
async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise<JobStatus> {
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset) {
return JobStatus.FAILED;
}
const webpPath = await this.generateThumbnail(asset, 'webp');
await this.assetRepository.update({ id: asset.id, webpPath });
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP);
await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS;
}
async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise<JobStatus> {
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset?.resizePath) {
if (!asset?.previewPath) {
return JobStatus.FAILED;
}
const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath);
const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath);
await this.assetRepository.update({ id: asset.id, thumbhash });
return JobStatus.SUCCESS;