1
0
forked from Cutlery/immich

thumbnail config

This commit is contained in:
mertalev 2024-03-24 01:57:02 -04:00
parent 5a1a6493c6
commit 35a2aa472a
No known key found for this signature in database
GPG Key ID: 9181CD92C0A1C5E3
4 changed files with 49 additions and 30 deletions

View File

@ -10,6 +10,7 @@ import {
AudioCodec, AudioCodec,
CQMode, CQMode,
Colorspace, Colorspace,
ImageFormat,
LogLevel, LogLevel,
SystemConfig, SystemConfig,
SystemConfigEntity, SystemConfigEntity,
@ -113,8 +114,10 @@ export const defaults = Object.freeze<SystemConfig>({
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
}, },
thumbnail: { thumbnail: {
webpSize: 250, thumbnailFormat: ImageFormat.WEBP,
jpegSize: 1440, thumbnailSize: 250,
previewFormat: ImageFormat.JPEG,
previewSize: 1440,
quality: 80, quality: 80,
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
}, },

View File

@ -22,6 +22,7 @@ import {
AudioCodec, AudioCodec,
CQMode, CQMode,
Colorspace, Colorspace,
ImageFormat,
LogLevel, LogLevel,
SystemConfig, SystemConfig,
ToneMapping, ToneMapping,
@ -386,17 +387,25 @@ export class SystemConfigThemeDto {
} }
class SystemConfigThumbnailDto { class SystemConfigThumbnailDto {
@IsInt() @IsEnum(ImageFormat)
@Min(1) @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
@Type(() => Number) thumbnailFormat!: ImageFormat;
@ApiProperty({ type: 'integer' })
webpSize!: number;
@IsInt() @IsInt()
@Min(1) @Min(1)
@Type(() => Number) @Type(() => Number)
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
jpegSize!: number; thumbnailSize!: number;
@IsEnum(ImageFormat)
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
previewFormat!: ImageFormat;
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
previewSize!: number;
@IsInt() @IsInt()
@Min(1) @Min(1)

View File

@ -165,6 +165,11 @@ export enum Colorspace {
P3 = 'p3', P3 = 'p3',
} }
export enum ImageFormat {
JPEG = 'jpeg',
WEBP = 'webp',
}
export enum LogLevel { export enum LogLevel {
VERBOSE = 'verbose', VERBOSE = 'verbose',
DEBUG = 'debug', DEBUG = 'debug',
@ -250,8 +255,10 @@ export interface SystemConfig {
template: string; template: string;
}; };
thumbnail: { thumbnail: {
webpSize: number; thumbnailFormat: ImageFormat;
jpegSize: number; thumbnailSize: number;
previewFormat: ImageFormat;
previewSize: number;
quality: number; quality: number;
colorspace: Colorspace; colorspace: Colorspace;
}; };

View File

@ -1,5 +1,5 @@
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; 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 { SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity';
@ -7,6 +7,7 @@ import { AssetPathType } from 'src/entities/move.entity';
import { import {
AudioCodec, AudioCodec,
Colorspace, Colorspace,
ImageFormat,
TranscodeHWAccel, TranscodeHWAccel,
TranscodePolicy, TranscodePolicy,
TranscodeTarget, TranscodeTarget,
@ -81,12 +82,12 @@ export class MediaService {
const jobs: JobItem[] = []; const jobs: JobItem[] = [];
for (const asset of assets) { for (const asset of assets) {
if (!asset.resizePath || force) { if (!asset.previewPath || force) {
jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } });
continue; continue;
} }
if (!asset.webpPath) { if (!asset.thumbnailPath) {
jobs.push({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } });
} }
if (!asset.thumbhash) { if (!asset.thumbhash) {
jobs.push({ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: { id: asset.id } });
@ -157,29 +158,28 @@ export class MediaService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL); await this.storageCore.moveAssetFile(asset, AssetPathType.PREVIEW);
await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL); await this.storageCore.moveAssetFile(asset, AssetPathType.THUMBNAIL);
await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO); await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO);
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const resizePath = await this.generateThumbnail(asset, 'jpeg'); const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG);
await this.assetRepository.update({ id: asset.id, resizePath }); await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') { private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
const { thumbnail, ffmpeg } = await this.configCore.getConfig(); const { thumbnail, ffmpeg } = await this.configCore.getConfig();
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize; const size = type === AssetPathType.PREVIEW ? thumbnail.previewSize : thumbnail.thumbnailSize;
const path = const path = StorageCore.getImagePath(asset, type);
format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset);
this.storageCore.ensureFolders(path); this.storageCore.ensureFolders(path);
switch (asset.type) { switch (asset.type) {
@ -214,24 +214,24 @@ export class MediaService {
return path; return path;
} }
async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const webpPath = await this.generateThumbnail(asset, 'webp'); const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP);
await this.assetRepository.update({ id: asset.id, webpPath }); await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset?.resizePath) { if (!asset?.previewPath) {
return JobStatus.FAILED; 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 }); await this.assetRepository.update({ id: asset.id, thumbhash });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;