diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index ba512571bb..a4a8ff2759 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -2425,6 +2425,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'storageTemplate': SystemConfigStorageTemplateDto; + /** + * + * @type {SystemConfigThumbnailDto} + * @memberof SystemConfigDto + */ + 'thumbnail': SystemConfigThumbnailDto; } /** * @@ -2716,6 +2722,25 @@ export interface SystemConfigTemplateStorageOptionDto { */ 'yearOptions': Array; } +/** + * + * @export + * @interface SystemConfigThumbnailDto + */ +export interface SystemConfigThumbnailDto { + /** + * + * @type {number} + * @memberof SystemConfigThumbnailDto + */ + 'jpegSize': number; + /** + * + * @type {number} + * @memberof SystemConfigThumbnailDto + */ + 'webpSize': number; +} /** * * @export diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index cab64a9e05..ea0993be8c 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -104,6 +104,7 @@ doc/SystemConfigOAuthDto.md doc/SystemConfigPasswordLoginDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.md +doc/SystemConfigThumbnailDto.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -236,6 +237,7 @@ lib/model/system_config_o_auth_dto.dart lib/model/system_config_password_login_dto.dart lib/model/system_config_storage_template_dto.dart lib/model/system_config_template_storage_option_dto.dart +lib/model/system_config_thumbnail_dto.dart lib/model/tag_response_dto.dart lib/model/tag_type_enum.dart lib/model/thumbnail_format.dart @@ -355,6 +357,7 @@ test/system_config_o_auth_dto_test.dart test/system_config_password_login_dto_test.dart test/system_config_storage_template_dto_test.dart test/system_config_template_storage_option_dto_test.dart +test/system_config_thumbnail_dto_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 814b08e270..2942d1d854 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index f1526854a1..ebccc31ebe 100644 Binary files a/mobile/openapi/doc/SystemConfigDto.md and b/mobile/openapi/doc/SystemConfigDto.md differ diff --git a/mobile/openapi/doc/SystemConfigThumbnailDto.md b/mobile/openapi/doc/SystemConfigThumbnailDto.md new file mode 100644 index 0000000000..892b863b3a Binary files /dev/null and b/mobile/openapi/doc/SystemConfigThumbnailDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index c16d603d14..644244a103 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index d8eb6a2c7d..fd1252cc78 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 6bc4e5e2a2..aefc97d1ba 100644 Binary files a/mobile/openapi/lib/model/system_config_dto.dart and b/mobile/openapi/lib/model/system_config_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart b/mobile/openapi/lib/model/system_config_thumbnail_dto.dart new file mode 100644 index 0000000000..54360074e3 Binary files /dev/null and b/mobile/openapi/lib/model/system_config_thumbnail_dto.dart differ diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 34b5a8cde3..946324282e 100644 Binary files a/mobile/openapi/test/system_config_dto_test.dart and b/mobile/openapi/test/system_config_dto_test.dart differ diff --git a/mobile/openapi/test/system_config_thumbnail_dto_test.dart b/mobile/openapi/test/system_config_thumbnail_dto_test.dart new file mode 100644 index 0000000000..3dd82cff7c Binary files /dev/null and b/mobile/openapi/test/system_config_thumbnail_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 049f0128ae..1aac93051b 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -6590,6 +6590,9 @@ }, "storageTemplate": { "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" + }, + "thumbnail": { + "$ref": "#/components/schemas/SystemConfigThumbnailDto" } }, "required": [ @@ -6597,7 +6600,8 @@ "oauth", "passwordLogin", "storageTemplate", - "job" + "job", + "thumbnail" ], "type": "object" }, @@ -6828,6 +6832,21 @@ ], "type": "object" }, + "SystemConfigThumbnailDto": { + "properties": { + "jpegSize": { + "type": "integer" + }, + "webpSize": { + "type": "integer" + } + }, + "required": [ + "webpSize", + "jpegSize" + ], + "type": "object" + }, "TagResponseDto": { "properties": { "id": { diff --git a/server/src/domain/media/media.constant.ts b/server/src/domain/media/media.constant.ts index 97dd3f1d72..3a8ee414b9 100644 --- a/server/src/domain/media/media.constant.ts +++ b/server/src/domain/media/media.constant.ts @@ -1,3 +1 @@ -export const JPEG_THUMBNAIL_SIZE = 1440; -export const WEBP_THUMBNAIL_SIZE = 250; export const FACE_THUMBNAIL_SIZE = 250; diff --git a/server/src/domain/media/media.service.ts b/server/src/domain/media/media.service.ts index 0bb96fd0bd..c4ba66cdd0 100644 --- a/server/src/domain/media/media.service.ts +++ b/server/src/domain/media/media.service.ts @@ -7,7 +7,6 @@ import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SI import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config'; import { SystemConfigCore } from '../system-config/system-config.core'; -import { JPEG_THUMBNAIL_SIZE, WEBP_THUMBNAIL_SIZE } from './media.constant'; import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository'; import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util'; @@ -63,11 +62,12 @@ export class MediaService { const resizePath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId); this.storageRepository.mkdirSync(resizePath); const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`); + const { thumbnail } = await this.configCore.getConfig(); switch (asset.type) { case AssetType.IMAGE: await this.mediaRepository.resize(asset.originalPath, jpegThumbnailPath, { - size: JPEG_THUMBNAIL_SIZE, + size: thumbnail.jpegSize, format: 'jpeg', }); this.logger.log(`Successfully generated image thumbnail ${asset.id}`); @@ -80,7 +80,7 @@ export class MediaService { return false; } const { ffmpeg } = await this.configCore.getConfig(); - const config = { ...ffmpeg, targetResolution: JPEG_THUMBNAIL_SIZE.toString(), twoPass: false }; + const config = { ...ffmpeg, targetResolution: thumbnail.jpegSize.toString(), twoPass: false }; const options = new ThumbnailConfig(config).getOptions(mainVideoStream); await this.mediaRepository.transcode(asset.originalPath, jpegThumbnailPath, options); this.logger.log(`Successfully generated video thumbnail ${asset.id}`); @@ -100,7 +100,8 @@ export class MediaService { const webpPath = asset.resizePath.replace('jpeg', 'webp').replace('jpg', 'webp'); - await this.mediaRepository.resize(asset.resizePath, webpPath, { size: WEBP_THUMBNAIL_SIZE, format: 'webp' }); + const { thumbnail } = await this.configCore.getConfig(); + await this.mediaRepository.resize(asset.resizePath, webpPath, { size: thumbnail.webpSize, format: 'webp' }); await this.assetRepository.save({ id: asset.id, webpPath }); return true; diff --git a/server/src/domain/system-config/dto/index.ts b/server/src/domain/system-config/dto/index.ts index fa494a7a8d..9eb2357964 100644 --- a/server/src/domain/system-config/dto/index.ts +++ b/server/src/domain/system-config/dto/index.ts @@ -2,4 +2,5 @@ export * from './system-config-ffmpeg.dto'; export * from './system-config-oauth.dto'; export * from './system-config-password-login.dto'; export * from './system-config-storage-template.dto'; +export * from './system-config-thumbnail.dto'; export * from './system-config.dto'; diff --git a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts b/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts new file mode 100644 index 0000000000..53d9d64a56 --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt } from 'class-validator'; + +export class SystemConfigThumbnailDto { + @IsInt() + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + webpSize!: number; + + @IsInt() + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + jpegSize!: number; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index bd2fb5b5e3..f34ebf7100 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -1,3 +1,4 @@ +import { SystemConfigThumbnailDto } from '@app/domain/system-config'; import { SystemConfig } from '@app/infra/entities'; import { Type } from 'class-transformer'; import { IsObject, ValidateNested } from 'class-validator'; @@ -32,6 +33,11 @@ export class SystemConfigDto { @ValidateNested() @IsObject() job!: SystemConfigJobDto; + + @Type(() => SystemConfigThumbnailDto) + @ValidateNested() + @IsObject() + thumbnail!: SystemConfigThumbnailDto; } export function mapConfig(config: SystemConfig): SystemConfigDto { diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 5a2dc03c99..80f650571c 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -64,6 +64,11 @@ export const defaults = Object.freeze({ storageTemplate: { template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, + + thumbnail: { + webpSize: 250, + jpegSize: 1440, + }, }); const singleton = new Subject(); diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 1b358771ae..bb510c05b3 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -65,6 +65,10 @@ const updatedConfig = Object.freeze({ storageTemplate: { template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, + thumbnail: { + webpSize: 250, + jpegSize: 1440, + }, }); describe(SystemConfigService.name, () => { diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index e8ae879427..ddfad682a7 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -52,6 +52,9 @@ export enum SystemConfigKey { PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled', STORAGE_TEMPLATE = 'storageTemplate.template', + + THUMBNAIL_WEBP_SIZE = 'thumbnail.webpSize', + THUMBNAIL_JPEG_SIZE = 'thumbnail.jpegSize', } export enum TranscodePolicy { @@ -121,4 +124,8 @@ export interface SystemConfig { storageTemplate: { template: string; }; + thumbnail: { + webpSize: number; + jpegSize: number; + }; } diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index 682b5a4000..b4c3f8abdf 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/infra/repositories/media.repository.ts @@ -12,7 +12,7 @@ export class MediaRepository implements IMediaRepository { private logger = new Logger(MediaRepository.name); crop(input: string, options: CropOptions): Promise { - return sharp(input, { failOnError: false }) + return sharp(input, { failOn: 'none' }) .extract({ left: options.left, top: options.top, @@ -23,7 +23,7 @@ export class MediaRepository implements IMediaRepository { } async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise { - await sharp(input, { failOnError: false }) + await sharp(input, { failOn: 'none' }) .resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }) .rotate() .toFormat(options.format) diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index ba512571bb..a4a8ff2759 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -2425,6 +2425,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'storageTemplate': SystemConfigStorageTemplateDto; + /** + * + * @type {SystemConfigThumbnailDto} + * @memberof SystemConfigDto + */ + 'thumbnail': SystemConfigThumbnailDto; } /** * @@ -2716,6 +2722,25 @@ export interface SystemConfigTemplateStorageOptionDto { */ 'yearOptions': Array; } +/** + * + * @export + * @interface SystemConfigThumbnailDto + */ +export interface SystemConfigThumbnailDto { + /** + * + * @type {number} + * @memberof SystemConfigThumbnailDto + */ + 'jpegSize': number; + /** + * + * @type {number} + * @memberof SystemConfigThumbnailDto + */ + 'webpSize': number; +} /** * * @export diff --git a/web/src/lib/components/admin-page/settings/setting-input-field.svelte b/web/src/lib/components/admin-page/settings/setting-input-field.svelte index 6f5711b4dc..65a5018e58 100644 --- a/web/src/lib/components/admin-page/settings/setting-input-field.svelte +++ b/web/src/lib/components/admin-page/settings/setting-input-field.svelte @@ -27,7 +27,7 @@ }; -
+
{#if required} @@ -45,7 +45,7 @@
{#if desc} -

+

{desc}

{/if} diff --git a/web/src/lib/components/admin-page/settings/setting-select.svelte b/web/src/lib/components/admin-page/settings/setting-select.svelte index 987cebccf7..4acc1c2ade 100644 --- a/web/src/lib/components/admin-page/settings/setting-select.svelte +++ b/web/src/lib/components/admin-page/settings/setting-select.svelte @@ -2,19 +2,23 @@ import { quintOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; - export let value: string; - export let options: { value: string; text: string }[]; + export let value: string | number; + export let options: { value: string | number; text: string }[]; export let label = ''; export let desc = ''; export let name = ''; export let isEdited = false; + export let number = false; const handleChange = (e: Event) => { value = (e.target as HTMLInputElement).value; + if (number) { + value = parseInt(value); + } }; -
+
@@ -29,7 +33,7 @@
{#if desc} -

+

{desc}

{/if} diff --git a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte b/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte new file mode 100644 index 0000000000..2bfce8e8d4 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte @@ -0,0 +1,121 @@ + + +
+ {#await getConfigs() then} +
+
+
+ + + +
+ +
+ +
+
+
+ {/await} +
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 24a1532fdb..59390b42d9 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; + import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte'; import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; @@ -22,6 +23,10 @@ {#await getConfig()} {:then configs} + + + +