From ddd4ec2d9ef1e5ad58df6b58866552dfdf87d728 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Aug 2023 09:39:51 -0500 Subject: [PATCH] feat(web/server): webp thumbnail size configurable (#3598) * feat(server/web): webp thumbnail size configurable * update api * add ui and fix test * lint * setting for jpeg size * feat: coerce to number * api * jpeg resolution --------- Co-authored-by: Jason Rasmussen --- cli/src/api/open-api/api.ts | 25 ++++ mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | Bin 18068 -> 18132 bytes mobile/openapi/doc/SystemConfigDto.md | Bin 782 -> 864 bytes .../openapi/doc/SystemConfigThumbnailDto.md | Bin 0 -> 448 bytes mobile/openapi/lib/api.dart | Bin 5804 -> 5851 bytes mobile/openapi/lib/api_client.dart | Bin 18373 -> 18473 bytes .../openapi/lib/model/system_config_dto.dart | Bin 3951 -> 4240 bytes .../model/system_config_thumbnail_dto.dart | Bin 0 -> 3194 bytes .../openapi/test/system_config_dto_test.dart | Bin 1064 -> 1185 bytes .../system_config_thumbnail_dto_test.dart | Bin 0 -> 691 bytes server/immich-openapi-specs.json | 21 ++- server/src/domain/media/media.constant.ts | 2 - server/src/domain/media/media.service.ts | 9 +- server/src/domain/system-config/dto/index.ts | 1 + .../dto/system-config-thumbnail.dto.ts | 15 +++ .../system-config/dto/system-config.dto.ts | 6 + .../system-config/system-config.core.ts | 5 + .../system-config.service.spec.ts | 4 + .../infra/entities/system-config.entity.ts | 7 + .../infra/repositories/media.repository.ts | 4 +- web/src/api/open-api/api.ts | 25 ++++ .../settings/setting-input-field.svelte | 4 +- .../admin-page/settings/setting-select.svelte | 12 +- .../thumbnail/thumbnail-settings.svelte | 121 ++++++++++++++++++ .../routes/admin/system-settings/+page.svelte | 5 + 26 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 mobile/openapi/doc/SystemConfigThumbnailDto.md create mode 100644 mobile/openapi/lib/model/system_config_thumbnail_dto.dart create mode 100644 mobile/openapi/test/system_config_thumbnail_dto_test.dart create mode 100644 server/src/domain/system-config/dto/system-config-thumbnail.dto.ts create mode 100644 web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte 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 814b08e270119ba902e0a590f24d2900c1178d46..2942d1d854a63407ec6e482fb94e23030cad7fbe 100644 GIT binary patch delta 42 pcmbQz%Xp=iaYK;PFcL4CX)^%LvJzte delta 11 ScmaFB*2lIXlWFp9rlkNIiv)`R diff --git a/mobile/openapi/doc/SystemConfigThumbnailDto.md b/mobile/openapi/doc/SystemConfigThumbnailDto.md new file mode 100644 index 0000000000000000000000000000000000000000..892b863b3ab4b239b90c5f2e2ed065b1fca83351 GIT binary patch literal 448 zcma)2!A=4(5WV*+CfNhhq|04T=yEU-LWCP3*|Hs$>bBE#hZu|>Z@ZF+CI)+H-@G?( zrmp}B=ykBABZFPJI`xe1f&AXK>%-CQytZA%Bt%ib6KMl{Kp3r=>Ag?$rkU`sxs1#wcSi<62MNU3XB zNcj@5UDNd5M`hlM961A delta 12 TcmcbuyGD1z7op9{!aJA&Cffx( diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index d8eb6a2c7d103e97483a25197e522bf11375f732..fd1252cc7846c10d28c577f6ac066fc3066b93ae 100644 GIT binary patch delta 44 scmX@w&$w~|WKO0NO2L)IC8@d2`FUxX z=^-!~my&#-TBx$kXP6`yRTb3KxD-Gjza%5I2w{kVtt~?79+WGu34H)pZkU{X>*7-OY?)$Nl%u#0fQ Z4cctRY0M_Bpnznw4$yhE)?Bq*Tmbf;WHSH& delta 46 zcmV+}0MY-DA@3fr4FZ!Y0>rZw0}}z0O$5!8H3gls=mjDHvo#0O0<%&ICk6(6I|_XY E3TqAy761SM 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 0000000000000000000000000000000000000000..54360074e381e57866dc66a330233ce7338cafb2 GIT binary patch literal 3194 zcmbVOZExE)5dQ98aRG{2!Blzcry;Gq7ELnrB{9%=0|p}yXqmE@$)rY7HN4dSeRrfN z%Zrr+4Uj~l?)`c0c%#u^G=lek%%-n?zqr2m`hIqC4woOlT*Pobh3n}IuBPW7FaJ70 zF_L_@WZI5jC%?QN(5qNUrFohwo#vwCXHd)9@MX$NzUI=#;a#jNrR`A-R&LnVq*ZNG z&Ht%|Ms>;7__tyj|68sN8rSBydZwhYOxjc|(V@r%*G}CWbyf<=Olf8PuSS;>rno2{{Y{XxlGtY$^MUPva&bF1Kf05HNreC4** z(f|X=H!$y$zXjpK&3J-}eNquw1qkH;TL_cfReW_L{^jwJZyUa-!nfTiMh)o4LQrWB zf}A^er@nEmv1};4P^zTFHM2^?cpOa^cRa&#*n;a*7*D{LH)CZ)(Zu1)_wW7*#3My| z`O?MgCEG;MO&cK`ii9{xBs@QenKE2)3k5U92esrFrE}BLlGrGD#*JZmOKd`d6x;~o z#G+80Bp5h_pZu5La{uLl^Bb#$T!B^oA#ybu_5nH%zQ!-YQ`{ytei8iL4xOJXR=xFR zV_0sn7eqEePoIpE=*M#-7m}55het4m2h#VuH@@OdVy=h5>+lxE59sti!4S$`Fr0Wn z?Rf6Y(!q|X_Cx#!CYRvHf&pO-d}C$J4dhy_8=G@?ge{Y!z>s=tj-|PcLDnU*m8iK9 z#&uE$^Gfp^o7>F=U1!K=lM>TY91~V#DNd9^Er|pEptlQubFsLpHz@cR0|XdWQ zV_@MN-~b#al4Q)-2QWpxRei71{OFA(!f^dhK=KUMtooB`HqB?-ioeBnm_$T~7&>^L zp7?n|$^ps8RfE)UuZm%k{?7ev7(f`b3}`C(MNpDGqjCZze!fI_4@&AIzIOv?rPYQl zO6+9FS!sB@3pMo>)!fvjb%gJ8GQah)5;$alK*JX!^G)1Gr?U`~icI54vra4b(C2YC$eClZWl84eC0MKYz zMd?MMbY~EroE6uf4LaSi(hm}{>L^AbWM3wf>du{Bb72s5;lMZ^MZ>{{R|BHg+cr3J zn%(0J+BG4?n;I7qT0H!SKR^~w^)_Ie(3{{UWJM_X%$|?2GnhE}4q{JWjfIf+^sYP8vr)%|3= z>Rd#`vi{zlkRpj3up?6$Pa4!7^KEM{i literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 34b5a8cde3e77a3903fc2ba1dd3a2d1b3983786f..946324282e7eb8a8d0baa39c5da700cf6a4b1763 100644 GIT binary patch delta 56 xcmZ3%v5<3v9Lr=rW?`|AjMChsyu{2Lmy&#i5*TN)J+mSwf|tvzHd&5^3jhy;5>x;H delta 18 ZcmZ3;xq@SZ919nhf?NJA>HhgQCYAn2x@yp;+CR}{K$ z;J5CK{z)tv8~WoQ@K8S+a8Clut+80&OOaENluf-BoT|vm;8RitH7C*S!FK@QTsa9| z0kzAd2;-CHZf_JgOQB_>x*ZQYDed&(7tkU|Sa?%dEa?kf0<=wCglsQhO9Z9e*4l1i pbr7!hA>3?5+IICu*EflyxDDCu`}`3$xK?Ko4#M*Yj@6QO!4Faw;N<`S literal 0 HcmV?d00001 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} + + + +