1
0
Fork 0
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:
Alex 2023-08-08 09:39:51 -05:00 committed by GitHub
parent 1812e8811b
commit ddd4ec2d9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 429 additions and 18 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View 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)

View file

@ -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';

View file

@ -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':

View file

@ -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',
}; };
} }

View 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',
};
}

View file

@ -41,6 +41,11 @@ void main() {
// TODO // TODO
}); });
// SystemConfigThumbnailDto thumbnail
test('to test the property `thumbnail`', () async {
// TODO
});
}); });

View 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
});
});
}

View file

@ -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": {

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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;
}

View file

@ -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 {

View file

@ -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>();

View file

@ -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, () => {

View file

@ -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;
};
} }

View file

@ -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)

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -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>

View file

@ -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"