mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	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 <jrasm91@gmail.com>
This commit is contained in:
		
							
								
								
									
										25
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -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<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SystemConfigThumbnailDto | ||||
|  */ | ||||
| export interface SystemConfigThumbnailDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof SystemConfigThumbnailDto | ||||
|      */ | ||||
|     'jpegSize': number; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof SystemConfigThumbnailDto | ||||
|      */ | ||||
|     'webpSize': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -267,6 +267,7 @@ Class | Method | HTTP request | Description | ||||
|  - [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md) | ||||
|  - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) | ||||
|  - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) | ||||
|  - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md) | ||||
|  - [TagResponseDto](doc//TagResponseDto.md) | ||||
|  - [TagTypeEnum](doc//TagTypeEnum.md) | ||||
|  - [ThumbnailFormat](doc//ThumbnailFormat.md) | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/SystemConfigDto.md
									
									
									
										generated
									
									
									
								
							| @@ -13,6 +13,7 @@ Name | Type | Description | Notes | ||||
| **oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) |  |  | ||||
| **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) |  |  | ||||
| **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) |  |  | ||||
| **thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) |  |  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										16
									
								
								mobile/openapi/doc/SystemConfigThumbnailDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								mobile/openapi/doc/SystemConfigThumbnailDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| # openapi.model.SystemConfigThumbnailDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **jpegSize** | **int** |  |  | ||||
| **webpSize** | **int** |  |  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -132,6 +132,7 @@ part 'model/system_config_o_auth_dto.dart'; | ||||
| part 'model/system_config_password_login_dto.dart'; | ||||
| part 'model/system_config_storage_template_dto.dart'; | ||||
| part 'model/system_config_template_storage_option_dto.dart'; | ||||
| part 'model/system_config_thumbnail_dto.dart'; | ||||
| part 'model/tag_response_dto.dart'; | ||||
| part 'model/tag_type_enum.dart'; | ||||
| part 'model/thumbnail_format.dart'; | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -359,6 +359,8 @@ class ApiClient { | ||||
|           return SystemConfigStorageTemplateDto.fromJson(value); | ||||
|         case 'SystemConfigTemplateStorageOptionDto': | ||||
|           return SystemConfigTemplateStorageOptionDto.fromJson(value); | ||||
|         case 'SystemConfigThumbnailDto': | ||||
|           return SystemConfigThumbnailDto.fromJson(value); | ||||
|         case 'TagResponseDto': | ||||
|           return TagResponseDto.fromJson(value); | ||||
|         case 'TagTypeEnum': | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/system_config_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -18,6 +18,7 @@ class SystemConfigDto { | ||||
|     required this.oauth, | ||||
|     required this.passwordLogin, | ||||
|     required this.storageTemplate, | ||||
|     required this.thumbnail, | ||||
|   }); | ||||
| 
 | ||||
|   SystemConfigFFmpegDto ffmpeg; | ||||
| @@ -30,13 +31,16 @@ class SystemConfigDto { | ||||
| 
 | ||||
|   SystemConfigStorageTemplateDto storageTemplate; | ||||
| 
 | ||||
|   SystemConfigThumbnailDto thumbnail; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && | ||||
|      other.ffmpeg == ffmpeg && | ||||
|      other.job == job && | ||||
|      other.oauth == oauth && | ||||
|      other.passwordLogin == passwordLogin && | ||||
|      other.storageTemplate == storageTemplate; | ||||
|      other.storageTemplate == storageTemplate && | ||||
|      other.thumbnail == thumbnail; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
| @@ -45,10 +49,11 @@ class SystemConfigDto { | ||||
|     (job.hashCode) + | ||||
|     (oauth.hashCode) + | ||||
|     (passwordLogin.hashCode) + | ||||
|     (storageTemplate.hashCode); | ||||
|     (storageTemplate.hashCode) + | ||||
|     (thumbnail.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate]'; | ||||
|   String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -57,6 +62,7 @@ class SystemConfigDto { | ||||
|       json[r'oauth'] = this.oauth; | ||||
|       json[r'passwordLogin'] = this.passwordLogin; | ||||
|       json[r'storageTemplate'] = this.storageTemplate; | ||||
|       json[r'thumbnail'] = this.thumbnail; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -73,6 +79,7 @@ class SystemConfigDto { | ||||
|         oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!, | ||||
|         passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, | ||||
|         storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, | ||||
|         thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
| @@ -125,6 +132,7 @@ class SystemConfigDto { | ||||
|     'oauth', | ||||
|     'passwordLogin', | ||||
|     'storageTemplate', | ||||
|     'thumbnail', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										106
									
								
								mobile/openapi/lib/model/system_config_thumbnail_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								mobile/openapi/lib/model/system_config_thumbnail_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class SystemConfigThumbnailDto { | ||||
|   /// Returns a new [SystemConfigThumbnailDto] instance. | ||||
|   SystemConfigThumbnailDto({ | ||||
|     required this.jpegSize, | ||||
|     required this.webpSize, | ||||
|   }); | ||||
| 
 | ||||
|   int jpegSize; | ||||
| 
 | ||||
|   int webpSize; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is SystemConfigThumbnailDto && | ||||
|      other.jpegSize == jpegSize && | ||||
|      other.webpSize == webpSize; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (jpegSize.hashCode) + | ||||
|     (webpSize.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'SystemConfigThumbnailDto[jpegSize=$jpegSize, webpSize=$webpSize]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'jpegSize'] = this.jpegSize; | ||||
|       json[r'webpSize'] = this.webpSize; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [SystemConfigThumbnailDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static SystemConfigThumbnailDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return SystemConfigThumbnailDto( | ||||
|         jpegSize: mapValueOfType<int>(json, r'jpegSize')!, | ||||
|         webpSize: mapValueOfType<int>(json, r'webpSize')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<SystemConfigThumbnailDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <SystemConfigThumbnailDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = SystemConfigThumbnailDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, SystemConfigThumbnailDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, SystemConfigThumbnailDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = SystemConfigThumbnailDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of SystemConfigThumbnailDto-objects as value to a dart map | ||||
|   static Map<String, List<SystemConfigThumbnailDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<SystemConfigThumbnailDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = SystemConfigThumbnailDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'jpegSize', | ||||
|     'webpSize', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/system_config_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -41,6 +41,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // SystemConfigThumbnailDto thumbnail | ||||
|     test('to test the property `thumbnail`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										32
									
								
								mobile/openapi/test/system_config_thumbnail_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								mobile/openapi/test/system_config_thumbnail_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for SystemConfigThumbnailDto | ||||
| void main() { | ||||
|   // final instance = SystemConfigThumbnailDto(); | ||||
| 
 | ||||
|   group('test SystemConfigThumbnailDto', () { | ||||
|     // int jpegSize | ||||
|     test('to test the property `jpegSize`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // int webpSize | ||||
|     test('to test the property `webpSize`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @@ -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": { | ||||
|   | ||||
| @@ -1,3 +1 @@ | ||||
| export const JPEG_THUMBNAIL_SIZE = 1440; | ||||
| export const WEBP_THUMBNAIL_SIZE = 250; | ||||
| export const FACE_THUMBNAIL_SIZE = 250; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -64,6 +64,11 @@ export const defaults = Object.freeze<SystemConfig>({ | ||||
|   storageTemplate: { | ||||
|     template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   }, | ||||
|  | ||||
|   thumbnail: { | ||||
|     webpSize: 250, | ||||
|     jpegSize: 1440, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const singleton = new Subject<SystemConfig>(); | ||||
|   | ||||
| @@ -65,6 +65,10 @@ const updatedConfig = Object.freeze<SystemConfig>({ | ||||
|   storageTemplate: { | ||||
|     template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', | ||||
|   }, | ||||
|   thumbnail: { | ||||
|     webpSize: 250, | ||||
|     jpegSize: 1440, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| describe(SystemConfigService.name, () => { | ||||
|   | ||||
| @@ -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; | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ export class MediaRepository implements IMediaRepository { | ||||
|   private logger = new Logger(MediaRepository.name); | ||||
|  | ||||
|   crop(input: string, options: CropOptions): Promise<Buffer> { | ||||
|     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<void> { | ||||
|     await sharp(input, { failOnError: false }) | ||||
|     await sharp(input, { failOn: 'none' }) | ||||
|       .resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }) | ||||
|       .rotate() | ||||
|       .toFormat(options.format) | ||||
|   | ||||
							
								
								
									
										25
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										25
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -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<string>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface SystemConfigThumbnailDto | ||||
|  */ | ||||
| export interface SystemConfigThumbnailDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof SystemConfigThumbnailDto | ||||
|      */ | ||||
|     'jpegSize': number; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof SystemConfigThumbnailDto | ||||
|      */ | ||||
|     'webpSize': number; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|   | ||||
| @@ -27,7 +27,7 @@ | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <div class="w-full"> | ||||
| <div class="mb-4 w-full"> | ||||
|   <div class={`flex h-[26px] place-items-center gap-1`}> | ||||
|     <label class={`immich-form-label text-sm`} for={label}>{label}</label> | ||||
|     {#if required} | ||||
| @@ -45,7 +45,7 @@ | ||||
|   </div> | ||||
|  | ||||
|   {#if desc} | ||||
|     <p class="immich-form-label pb-2 text-xs" id="{label}-desc"> | ||||
|     <p class="immich-form-label pb-2 text-sm" id="{label}-desc"> | ||||
|       {desc} | ||||
|     </p> | ||||
|   {/if} | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <div class="w-full"> | ||||
| <div class="mb-4 w-full"> | ||||
|   <div class={`flex h-[26px] place-items-center gap-1`}> | ||||
|     <label class={`immich-form-label text-sm`} for="{name}-select">{label}</label> | ||||
|  | ||||
| @@ -29,7 +33,7 @@ | ||||
|   </div> | ||||
|  | ||||
|   {#if desc} | ||||
|     <p class="immich-form-label pb-2 text-xs" id="{name}-desc"> | ||||
|     <p class="immich-form-label pb-2 text-sm" id="{name}-desc"> | ||||
|       {desc} | ||||
|     </p> | ||||
|   {/if} | ||||
|   | ||||
| @@ -0,0 +1,121 @@ | ||||
| <script lang="ts"> | ||||
|   import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte'; | ||||
|   import { api, SystemConfigThumbnailDto } from '@api'; | ||||
|   import { fade } from 'svelte/transition'; | ||||
|   import { isEqual } from 'lodash-es'; | ||||
|   import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte'; | ||||
|   import { | ||||
|     notificationController, | ||||
|     NotificationType, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|  | ||||
|   export let thumbnailConfig: SystemConfigThumbnailDto; // this is the config that is being edited | ||||
|  | ||||
|   let savedConfig: SystemConfigThumbnailDto; | ||||
|   let defaultConfig: SystemConfigThumbnailDto; | ||||
|  | ||||
|   async function getConfigs() { | ||||
|     [savedConfig, defaultConfig] = await Promise.all([ | ||||
|       api.systemConfigApi.getConfig().then((res) => res.data.thumbnail), | ||||
|       api.systemConfigApi.getDefaults().then((res) => res.data.thumbnail), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   async function reset() { | ||||
|     const { data: resetConfig } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
|     thumbnailConfig = { ...resetConfig.thumbnail }; | ||||
|     savedConfig = { ...resetConfig.thumbnail }; | ||||
|  | ||||
|     notificationController.show({ | ||||
|       message: 'Reset thumbnail settings to the recent saved settings', | ||||
|       type: NotificationType.Info, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async function resetToDefault() { | ||||
|     const { data: configs } = await api.systemConfigApi.getDefaults(); | ||||
|  | ||||
|     thumbnailConfig = { ...configs.thumbnail }; | ||||
|     defaultConfig = { ...configs.thumbnail }; | ||||
|  | ||||
|     notificationController.show({ | ||||
|       message: 'Reset thumbnail settings to default', | ||||
|       type: NotificationType.Info, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async function saveSetting() { | ||||
|     try { | ||||
|       const { data: configs } = await api.systemConfigApi.getConfig(); | ||||
|  | ||||
|       const result = await api.systemConfigApi.updateConfig({ | ||||
|         systemConfigDto: { | ||||
|           ...configs, | ||||
|           thumbnail: thumbnailConfig, | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       thumbnailConfig = { ...result.data.thumbnail }; | ||||
|       savedConfig = { ...result.data.thumbnail }; | ||||
|  | ||||
|       notificationController.show({ | ||||
|         message: 'Thumbnail settings saved', | ||||
|         type: NotificationType.Info, | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       console.error('Error [thumbnail-settings] [saveSetting]', e); | ||||
|       notificationController.show({ | ||||
|         message: 'Unable to save settings', | ||||
|         type: NotificationType.Error, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <div> | ||||
|   {#await getConfigs() then} | ||||
|     <div in:fade={{ duration: 500 }}> | ||||
|       <form autocomplete="off" on:submit|preventDefault> | ||||
|         <div class="ml-4 mt-4 flex flex-col gap-4"> | ||||
|           <SettingSelect | ||||
|             label="WEBP RESOLUTION" | ||||
|             desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness." | ||||
|             number | ||||
|             bind:value={thumbnailConfig.webpSize} | ||||
|             options={[ | ||||
|               { value: 1080, text: '1080p' }, | ||||
|               { value: 720, text: '720p' }, | ||||
|               { value: 480, text: '480p' }, | ||||
|               { value: 250, text: '250p' }, | ||||
|             ]} | ||||
|             name="resolution" | ||||
|             isEdited={!(thumbnailConfig.webpSize === savedConfig.webpSize)} | ||||
|           /> | ||||
|  | ||||
|           <SettingSelect | ||||
|             label="JPEG RESOLUTION" | ||||
|             desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness." | ||||
|             number | ||||
|             bind:value={thumbnailConfig.jpegSize} | ||||
|             options={[ | ||||
|               { value: 2160, text: '4K' }, | ||||
|               { value: 1440, text: '1440p' }, | ||||
|             ]} | ||||
|             name="resolution" | ||||
|             isEdited={!(thumbnailConfig.jpegSize === savedConfig.jpegSize)} | ||||
|           /> | ||||
|         </div> | ||||
|  | ||||
|         <div class="ml-4"> | ||||
|           <SettingButtonsRow | ||||
|             on:reset={reset} | ||||
|             on:save={saveSetting} | ||||
|             on:reset-to-default={resetToDefault} | ||||
|             showResetToDefault={!isEqual(savedConfig, defaultConfig)} | ||||
|           /> | ||||
|         </div> | ||||
|       </form> | ||||
|     </div> | ||||
|   {/await} | ||||
| </div> | ||||
| @@ -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()} | ||||
|     <LoadingSpinner /> | ||||
|   {:then configs} | ||||
|     <SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes"> | ||||
|       <ThumbnailSettings thumbnailConfig={configs.thumbnail} /> | ||||
|     </SettingAccordion> | ||||
|  | ||||
|     <SettingAccordion | ||||
|       title="FFmpeg Settings" | ||||
|       subtitle="Manage the resolution and encoding information of the video files" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user