mirror of
https://github.com/immich-app/immich.git
synced 2025-03-01 15:11:21 +01: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:
parent
1812e8811b
commit
ddd4ec2d9e
26 changed files with 429 additions and 18 deletions
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
|
* @memberof SystemConfigDto
|
||||||
*/
|
*/
|
||||||
'storageTemplate': SystemConfigStorageTemplateDto;
|
'storageTemplate': SystemConfigStorageTemplateDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {SystemConfigThumbnailDto}
|
||||||
|
* @memberof SystemConfigDto
|
||||||
|
*/
|
||||||
|
'thumbnail': SystemConfigThumbnailDto;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -2716,6 +2722,25 @@ export interface SystemConfigTemplateStorageOptionDto {
|
||||||
*/
|
*/
|
||||||
'yearOptions': Array<string>;
|
'yearOptions': Array<string>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SystemConfigThumbnailDto
|
||||||
|
*/
|
||||||
|
export interface SystemConfigThumbnailDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof SystemConfigThumbnailDto
|
||||||
|
*/
|
||||||
|
'jpegSize': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof SystemConfigThumbnailDto
|
||||||
|
*/
|
||||||
|
'webpSize': number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @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/SystemConfigPasswordLoginDto.md
|
||||||
doc/SystemConfigStorageTemplateDto.md
|
doc/SystemConfigStorageTemplateDto.md
|
||||||
doc/SystemConfigTemplateStorageOptionDto.md
|
doc/SystemConfigTemplateStorageOptionDto.md
|
||||||
|
doc/SystemConfigThumbnailDto.md
|
||||||
doc/TagApi.md
|
doc/TagApi.md
|
||||||
doc/TagResponseDto.md
|
doc/TagResponseDto.md
|
||||||
doc/TagTypeEnum.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_password_login_dto.dart
|
||||||
lib/model/system_config_storage_template_dto.dart
|
lib/model/system_config_storage_template_dto.dart
|
||||||
lib/model/system_config_template_storage_option_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_response_dto.dart
|
||||||
lib/model/tag_type_enum.dart
|
lib/model/tag_type_enum.dart
|
||||||
lib/model/thumbnail_format.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_password_login_dto_test.dart
|
||||||
test/system_config_storage_template_dto_test.dart
|
test/system_config_storage_template_dto_test.dart
|
||||||
test/system_config_template_storage_option_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_api_test.dart
|
||||||
test/tag_response_dto_test.dart
|
test/tag_response_dto_test.dart
|
||||||
test/tag_type_enum_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)
|
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
|
||||||
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
|
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
|
||||||
- [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md)
|
- [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md)
|
||||||
|
- [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md)
|
||||||
- [TagResponseDto](doc//TagResponseDto.md)
|
- [TagResponseDto](doc//TagResponseDto.md)
|
||||||
- [TagTypeEnum](doc//TagTypeEnum.md)
|
- [TagTypeEnum](doc//TagTypeEnum.md)
|
||||||
- [ThumbnailFormat](doc//ThumbnailFormat.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) | |
|
**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) | |
|
||||||
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | |
|
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | |
|
||||||
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.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)
|
[[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_password_login_dto.dart';
|
||||||
part 'model/system_config_storage_template_dto.dart';
|
part 'model/system_config_storage_template_dto.dart';
|
||||||
part 'model/system_config_template_storage_option_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_response_dto.dart';
|
||||||
part 'model/tag_type_enum.dart';
|
part 'model/tag_type_enum.dart';
|
||||||
part 'model/thumbnail_format.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);
|
return SystemConfigStorageTemplateDto.fromJson(value);
|
||||||
case 'SystemConfigTemplateStorageOptionDto':
|
case 'SystemConfigTemplateStorageOptionDto':
|
||||||
return SystemConfigTemplateStorageOptionDto.fromJson(value);
|
return SystemConfigTemplateStorageOptionDto.fromJson(value);
|
||||||
|
case 'SystemConfigThumbnailDto':
|
||||||
|
return SystemConfigThumbnailDto.fromJson(value);
|
||||||
case 'TagResponseDto':
|
case 'TagResponseDto':
|
||||||
return TagResponseDto.fromJson(value);
|
return TagResponseDto.fromJson(value);
|
||||||
case 'TagTypeEnum':
|
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.oauth,
|
||||||
required this.passwordLogin,
|
required this.passwordLogin,
|
||||||
required this.storageTemplate,
|
required this.storageTemplate,
|
||||||
|
required this.thumbnail,
|
||||||
});
|
});
|
||||||
|
|
||||||
SystemConfigFFmpegDto ffmpeg;
|
SystemConfigFFmpegDto ffmpeg;
|
||||||
|
@ -30,13 +31,16 @@ class SystemConfigDto {
|
||||||
|
|
||||||
SystemConfigStorageTemplateDto storageTemplate;
|
SystemConfigStorageTemplateDto storageTemplate;
|
||||||
|
|
||||||
|
SystemConfigThumbnailDto thumbnail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
|
||||||
other.ffmpeg == ffmpeg &&
|
other.ffmpeg == ffmpeg &&
|
||||||
other.job == job &&
|
other.job == job &&
|
||||||
other.oauth == oauth &&
|
other.oauth == oauth &&
|
||||||
other.passwordLogin == passwordLogin &&
|
other.passwordLogin == passwordLogin &&
|
||||||
other.storageTemplate == storageTemplate;
|
other.storageTemplate == storageTemplate &&
|
||||||
|
other.thumbnail == thumbnail;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
|
@ -45,10 +49,11 @@ class SystemConfigDto {
|
||||||
(job.hashCode) +
|
(job.hashCode) +
|
||||||
(oauth.hashCode) +
|
(oauth.hashCode) +
|
||||||
(passwordLogin.hashCode) +
|
(passwordLogin.hashCode) +
|
||||||
(storageTemplate.hashCode);
|
(storageTemplate.hashCode) +
|
||||||
|
(thumbnail.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -57,6 +62,7 @@ class SystemConfigDto {
|
||||||
json[r'oauth'] = this.oauth;
|
json[r'oauth'] = this.oauth;
|
||||||
json[r'passwordLogin'] = this.passwordLogin;
|
json[r'passwordLogin'] = this.passwordLogin;
|
||||||
json[r'storageTemplate'] = this.storageTemplate;
|
json[r'storageTemplate'] = this.storageTemplate;
|
||||||
|
json[r'thumbnail'] = this.thumbnail;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +79,7 @@ class SystemConfigDto {
|
||||||
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
|
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
|
||||||
passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
|
passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
|
||||||
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
|
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
|
||||||
|
thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -125,6 +132,7 @@ class SystemConfigDto {
|
||||||
'oauth',
|
'oauth',
|
||||||
'passwordLogin',
|
'passwordLogin',
|
||||||
'storageTemplate',
|
'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
|
// 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": {
|
"storageTemplate": {
|
||||||
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
|
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigThumbnailDto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -6597,7 +6600,8 @@
|
||||||
"oauth",
|
"oauth",
|
||||||
"passwordLogin",
|
"passwordLogin",
|
||||||
"storageTemplate",
|
"storageTemplate",
|
||||||
"job"
|
"job",
|
||||||
|
"thumbnail"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
@ -6828,6 +6832,21 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SystemConfigThumbnailDto": {
|
||||||
|
"properties": {
|
||||||
|
"jpegSize": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"webpSize": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"webpSize",
|
||||||
|
"jpegSize"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"TagResponseDto": {
|
"TagResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
export const JPEG_THUMBNAIL_SIZE = 1440;
|
|
||||||
export const WEBP_THUMBNAIL_SIZE = 250;
|
|
||||||
export const FACE_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 { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
||||||
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
||||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
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 { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository';
|
||||||
import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util';
|
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);
|
const resizePath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
|
||||||
this.storageRepository.mkdirSync(resizePath);
|
this.storageRepository.mkdirSync(resizePath);
|
||||||
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
|
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
|
||||||
|
const { thumbnail } = await this.configCore.getConfig();
|
||||||
|
|
||||||
switch (asset.type) {
|
switch (asset.type) {
|
||||||
case AssetType.IMAGE:
|
case AssetType.IMAGE:
|
||||||
await this.mediaRepository.resize(asset.originalPath, jpegThumbnailPath, {
|
await this.mediaRepository.resize(asset.originalPath, jpegThumbnailPath, {
|
||||||
size: JPEG_THUMBNAIL_SIZE,
|
size: thumbnail.jpegSize,
|
||||||
format: 'jpeg',
|
format: 'jpeg',
|
||||||
});
|
});
|
||||||
this.logger.log(`Successfully generated image thumbnail ${asset.id}`);
|
this.logger.log(`Successfully generated image thumbnail ${asset.id}`);
|
||||||
|
@ -80,7 +80,7 @@ export class MediaService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { ffmpeg } = await this.configCore.getConfig();
|
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);
|
const options = new ThumbnailConfig(config).getOptions(mainVideoStream);
|
||||||
await this.mediaRepository.transcode(asset.originalPath, jpegThumbnailPath, options);
|
await this.mediaRepository.transcode(asset.originalPath, jpegThumbnailPath, options);
|
||||||
this.logger.log(`Successfully generated video thumbnail ${asset.id}`);
|
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');
|
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 });
|
await this.assetRepository.save({ id: asset.id, webpPath });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -2,4 +2,5 @@ export * from './system-config-ffmpeg.dto';
|
||||||
export * from './system-config-oauth.dto';
|
export * from './system-config-oauth.dto';
|
||||||
export * from './system-config-password-login.dto';
|
export * from './system-config-password-login.dto';
|
||||||
export * from './system-config-storage-template.dto';
|
export * from './system-config-storage-template.dto';
|
||||||
|
export * from './system-config-thumbnail.dto';
|
||||||
export * from './system-config.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 { SystemConfig } from '@app/infra/entities';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsObject, ValidateNested } from 'class-validator';
|
import { IsObject, ValidateNested } from 'class-validator';
|
||||||
|
@ -32,6 +33,11 @@ export class SystemConfigDto {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
job!: SystemConfigJobDto;
|
job!: SystemConfigJobDto;
|
||||||
|
|
||||||
|
@Type(() => SystemConfigThumbnailDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
thumbnail!: SystemConfigThumbnailDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapConfig(config: SystemConfig): SystemConfigDto {
|
export function mapConfig(config: SystemConfig): SystemConfigDto {
|
||||||
|
|
|
@ -64,6 +64,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||||
storageTemplate: {
|
storageTemplate: {
|
||||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
thumbnail: {
|
||||||
|
webpSize: 250,
|
||||||
|
jpegSize: 1440,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const singleton = new Subject<SystemConfig>();
|
const singleton = new Subject<SystemConfig>();
|
||||||
|
|
|
@ -65,6 +65,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||||
storageTemplate: {
|
storageTemplate: {
|
||||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||||
},
|
},
|
||||||
|
thumbnail: {
|
||||||
|
webpSize: 250,
|
||||||
|
jpegSize: 1440,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(SystemConfigService.name, () => {
|
describe(SystemConfigService.name, () => {
|
||||||
|
|
|
@ -52,6 +52,9 @@ export enum SystemConfigKey {
|
||||||
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
|
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
|
||||||
|
|
||||||
STORAGE_TEMPLATE = 'storageTemplate.template',
|
STORAGE_TEMPLATE = 'storageTemplate.template',
|
||||||
|
|
||||||
|
THUMBNAIL_WEBP_SIZE = 'thumbnail.webpSize',
|
||||||
|
THUMBNAIL_JPEG_SIZE = 'thumbnail.jpegSize',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TranscodePolicy {
|
export enum TranscodePolicy {
|
||||||
|
@ -121,4 +124,8 @@ export interface SystemConfig {
|
||||||
storageTemplate: {
|
storageTemplate: {
|
||||||
template: string;
|
template: string;
|
||||||
};
|
};
|
||||||
|
thumbnail: {
|
||||||
|
webpSize: number;
|
||||||
|
jpegSize: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ export class MediaRepository implements IMediaRepository {
|
||||||
private logger = new Logger(MediaRepository.name);
|
private logger = new Logger(MediaRepository.name);
|
||||||
|
|
||||||
crop(input: string, options: CropOptions): Promise<Buffer> {
|
crop(input: string, options: CropOptions): Promise<Buffer> {
|
||||||
return sharp(input, { failOnError: false })
|
return sharp(input, { failOn: 'none' })
|
||||||
.extract({
|
.extract({
|
||||||
left: options.left,
|
left: options.left,
|
||||||
top: options.top,
|
top: options.top,
|
||||||
|
@ -23,7 +23,7 @@ export class MediaRepository implements IMediaRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void> {
|
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 })
|
.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true })
|
||||||
.rotate()
|
.rotate()
|
||||||
.toFormat(options.format)
|
.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
|
* @memberof SystemConfigDto
|
||||||
*/
|
*/
|
||||||
'storageTemplate': SystemConfigStorageTemplateDto;
|
'storageTemplate': SystemConfigStorageTemplateDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {SystemConfigThumbnailDto}
|
||||||
|
* @memberof SystemConfigDto
|
||||||
|
*/
|
||||||
|
'thumbnail': SystemConfigThumbnailDto;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -2716,6 +2722,25 @@ export interface SystemConfigTemplateStorageOptionDto {
|
||||||
*/
|
*/
|
||||||
'yearOptions': Array<string>;
|
'yearOptions': Array<string>;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @interface SystemConfigThumbnailDto
|
||||||
|
*/
|
||||||
|
export interface SystemConfigThumbnailDto {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof SystemConfigThumbnailDto
|
||||||
|
*/
|
||||||
|
'jpegSize': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof SystemConfigThumbnailDto
|
||||||
|
*/
|
||||||
|
'webpSize': number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="mb-4 w-full">
|
||||||
<div class={`flex h-[26px] place-items-center gap-1`}>
|
<div class={`flex h-[26px] place-items-center gap-1`}>
|
||||||
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
|
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
|
||||||
{#if required}
|
{#if required}
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if desc}
|
{#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}
|
{desc}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -2,19 +2,23 @@
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
|
||||||
export let value: string;
|
export let value: string | number;
|
||||||
export let options: { value: string; text: string }[];
|
export let options: { value: string | number; text: string }[];
|
||||||
export let label = '';
|
export let label = '';
|
||||||
export let desc = '';
|
export let desc = '';
|
||||||
export let name = '';
|
export let name = '';
|
||||||
export let isEdited = false;
|
export let isEdited = false;
|
||||||
|
export let number = false;
|
||||||
|
|
||||||
const handleChange = (e: Event) => {
|
const handleChange = (e: Event) => {
|
||||||
value = (e.target as HTMLInputElement).value;
|
value = (e.target as HTMLInputElement).value;
|
||||||
|
if (number) {
|
||||||
|
value = parseInt(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="mb-4 w-full">
|
||||||
<div class={`flex h-[26px] place-items-center gap-1`}>
|
<div class={`flex h-[26px] place-items-center gap-1`}>
|
||||||
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
|
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
|
||||||
|
|
||||||
|
@ -29,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if desc}
|
{#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}
|
{desc}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/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 { page } from '$app/stores';
|
||||||
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
|
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 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 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 PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
|
||||||
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
||||||
|
@ -22,6 +23,10 @@
|
||||||
{#await getConfig()}
|
{#await getConfig()}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then configs}
|
{:then configs}
|
||||||
|
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
|
||||||
|
<ThumbnailSettings thumbnailConfig={configs.thumbnail} />
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
title="FFmpeg Settings"
|
title="FFmpeg Settings"
|
||||||
subtitle="Manage the resolution and encoding information of the video files"
|
subtitle="Manage the resolution and encoding information of the video files"
|
||||||
|
|
Loading…
Add table
Reference in a new issue