From 8edc2fb46f9b3d72c666ffaa4e9ebffd24f2d3df Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:56:56 -0400 Subject: [PATCH] refactor(server): decouple generated images from image formats (#8246) * rename thumbnail config update target paths, fix tests rename to image settings replace legacy enum better typing update sql update api remove config option fix * update docs * update other thumbnail configs in migration * keep legacy enum for now * fix jumbled job names * fix jumbled job names in tests * rename thumbhash job * rename dto * fix tests * preserve order * remove unused import * keep old fields in dto, marked deprecated * update sql --------- Co-authored-by: Alex Tran --- docs/docs/guides/database-queries.md | 2 +- docs/docs/install/config-file.md | 8 +- mobile/openapi/.openapi-generator/FILES | 9 +- mobile/openapi/README.md | 3 +- mobile/openapi/doc/AssetApi.md | 8 +- mobile/openapi/doc/ImageFormat.md | 14 ++ mobile/openapi/doc/MetadataSearchDto.md | 2 + mobile/openapi/doc/SystemConfigDto.md | 2 +- ...humbnailDto.md => SystemConfigImageDto.md} | 8 +- mobile/openapi/lib/api.dart | 3 +- mobile/openapi/lib/api/asset_api.dart | 20 ++- mobile/openapi/lib/api_client.dart | 6 +- mobile/openapi/lib/api_helper.dart | 3 + mobile/openapi/lib/model/image_format.dart | 85 +++++++++++ .../lib/model/metadata_search_dto.dart | 36 ++++- mobile/openapi/lib/model/path_type.dart | 12 +- .../openapi/lib/model/system_config_dto.dart | 18 +-- .../lib/model/system_config_image_dto.dart | 138 ++++++++++++++++++ .../model/system_config_thumbnail_dto.dart | 122 ---------------- mobile/openapi/test/asset_api_test.dart | 2 +- mobile/openapi/test/image_format_test.dart | 21 +++ .../test/metadata_search_dto_test.dart | 10 ++ .../openapi/test/system_config_dto_test.dart | 10 +- ...dart => system_config_image_dto_test.dart} | 24 ++- open-api/immich-openapi-specs.json | 99 +++++++++---- open-api/typescript-sdk/src/fetch-client.ts | 40 +++-- server/src/cores/storage.core.ts | 65 ++++----- server/src/cores/system-config.core.ts | 9 +- server/src/dtos/asset-response.dto.ts | 4 +- server/src/dtos/search.dto.ts | 12 ++ server/src/dtos/system-config.dto.ts | 27 ++-- server/src/entities/asset.entity.ts | 4 +- server/src/entities/move.entity.ts | 4 +- server/src/entities/system-config.entity.ts | 13 +- server/src/interfaces/job.interface.ts | 12 +- server/src/interfaces/media.interface.ts | 4 +- server/src/interfaces/search.interface.ts | 4 +- .../1711257900274-RenameWebpJpegPaths.ts | 51 +++++++ server/src/queries/asset.repository.sql | 42 +++--- server/src/queries/person.repository.sql | 12 +- server/src/queries/search.repository.sql | 20 +-- server/src/queries/shared.link.repository.sql | 12 +- .../src/repositories/asset-v1.repository.ts | 4 +- server/src/repositories/asset.repository.ts | 18 +-- server/src/repositories/job.repository.ts | 6 +- server/src/services/asset-v1.service.spec.ts | 4 +- server/src/services/asset-v1.service.ts | 22 +-- server/src/services/asset.service.spec.ts | 12 +- server/src/services/asset.service.ts | 4 +- server/src/services/audit.service.ts | 22 +-- server/src/services/job.service.spec.ts | 20 +-- server/src/services/job.service.ts | 10 +- server/src/services/media.service.spec.ts | 123 +++++++++------- server/src/services/media.service.ts | 53 +++---- server/src/services/microservices.service.ts | 6 +- server/src/services/person.service.spec.ts | 2 +- server/src/services/person.service.ts | 19 +-- server/src/services/search.service.ts | 3 + .../src/services/smart-info.service.spec.ts | 4 +- server/src/services/smart-info.service.ts | 4 +- .../services/system-config.service.spec.ts | 9 +- server/src/utils/database.ts | 2 +- server/test/fixtures/asset.stub.ts | 68 ++++----- server/test/fixtures/shared-link.stub.ts | 4 +- .../image-settings.svelte} | 28 ++-- .../routes/admin/system-settings/+page.svelte | 16 +- 66 files changed, 916 insertions(+), 547 deletions(-) create mode 100644 mobile/openapi/doc/ImageFormat.md rename mobile/openapi/doc/{SystemConfigThumbnailDto.md => SystemConfigImageDto.md} (65%) create mode 100644 mobile/openapi/lib/model/image_format.dart create mode 100644 mobile/openapi/lib/model/system_config_image_dto.dart delete mode 100644 mobile/openapi/lib/model/system_config_thumbnail_dto.dart create mode 100644 mobile/openapi/test/image_format_test.dart rename mobile/openapi/test/{system_config_thumbnail_dto_test.dart => system_config_image_dto_test.dart} (53%) create mode 100644 server/src/migrations/1711257900274-RenameWebpJpegPaths.ts rename web/src/lib/components/admin-page/settings/{thumbnail/thumbnail-settings.svelte => image/image-settings.svelte} (75%) diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index fe369f899eee1..e8252f25d63b4 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -45,7 +45,7 @@ SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "ex ``` ```sql title="Without thumbnails" -SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL; +SELECT * FROM "assets" WHERE "assets"."previewPath" IS NULL OR "assets"."thumbnailPath" IS NULL; ``` ```sql title="By type" diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 9a1d1acb1b49f..a890d674bc1f4 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -114,9 +114,11 @@ The default configuration looks like this: "hashVerificationEnabled": true, "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" }, - "thumbnail": { - "webpSize": 250, - "jpegSize": 1440, + "image": { + "thumbnailFormat": "webp", + "thumbnailSize": 250, + "previewFormat": "jpeg", + "previewSize": 1440, "quality": 80, "colorspace": "p3" }, diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 9ec77670fbda4..795943e299fed 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -72,6 +72,7 @@ doc/FileChecksumResponseDto.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md +doc/ImageFormat.md doc/JobApi.md doc/JobCommand.md doc/JobCommandDto.md @@ -145,6 +146,7 @@ doc/SmartSearchDto.md doc/SystemConfigApi.md doc/SystemConfigDto.md doc/SystemConfigFFmpegDto.md +doc/SystemConfigImageDto.md doc/SystemConfigJobDto.md doc/SystemConfigLibraryDto.md doc/SystemConfigLibraryScanDto.md @@ -160,7 +162,6 @@ doc/SystemConfigServerDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md -doc/SystemConfigThumbnailDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md doc/TagApi.md @@ -284,6 +285,7 @@ lib/model/file_checksum_response_dto.dart lib/model/file_report_dto.dart lib/model/file_report_fix_dto.dart lib/model/file_report_item_dto.dart +lib/model/image_format.dart lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts_dto.dart @@ -348,6 +350,7 @@ lib/model/smart_info_response_dto.dart lib/model/smart_search_dto.dart lib/model/system_config_dto.dart lib/model/system_config_f_fmpeg_dto.dart +lib/model/system_config_image_dto.dart lib/model/system_config_job_dto.dart lib/model/system_config_library_dto.dart lib/model/system_config_library_scan_dto.dart @@ -363,7 +366,6 @@ lib/model/system_config_server_dto.dart lib/model/system_config_storage_template_dto.dart lib/model/system_config_template_storage_option_dto.dart lib/model/system_config_theme_dto.dart -lib/model/system_config_thumbnail_dto.dart lib/model/system_config_trash_dto.dart lib/model/system_config_user_dto.dart lib/model/tag_response_dto.dart @@ -461,6 +463,7 @@ test/file_checksum_response_dto_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart +test/image_format_test.dart test/job_api_test.dart test/job_command_dto_test.dart test/job_command_test.dart @@ -534,6 +537,7 @@ test/smart_search_dto_test.dart test/system_config_api_test.dart test/system_config_dto_test.dart test/system_config_f_fmpeg_dto_test.dart +test/system_config_image_dto_test.dart test/system_config_job_dto_test.dart test/system_config_library_dto_test.dart test/system_config_library_scan_dto_test.dart @@ -549,7 +553,6 @@ test/system_config_server_dto_test.dart test/system_config_storage_template_dto_test.dart test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart -test/system_config_thumbnail_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart test/tag_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index bfdac06c48e69..a46f2383e5d04 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -277,6 +277,7 @@ Class | Method | HTTP request | Description - [FileReportDto](doc//FileReportDto.md) - [FileReportFixDto](doc//FileReportFixDto.md) - [FileReportItemDto](doc//FileReportItemDto.md) + - [ImageFormat](doc//ImageFormat.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) @@ -341,6 +342,7 @@ Class | Method | HTTP request | Description - [SmartSearchDto](doc//SmartSearchDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) - [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md) @@ -356,7 +358,6 @@ Class | Method | HTTP request | Description - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [SystemConfigUserDto](doc//SystemConfigUserDto.md) - [TagResponseDto](doc//TagResponseDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 0778485c37dfa..297a4cdba6acc 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -886,7 +886,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **searchAssets** -> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) +> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) @@ -936,11 +936,13 @@ final originalFileName = originalFileName_example; // String | final originalPath = originalPath_example; // String | final page = 8.14; // num | final personIds = []; // List | +final previewPath = previewPath_example; // String | final resizePath = resizePath_example; // String | final size = 8.14; // num | final state = state_example; // String | final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final thumbnailPath = thumbnailPath_example; // String | final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final type = ; // AssetTypeEnum | @@ -954,7 +956,7 @@ final withPeople = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); + final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->searchAssets: $e\n'); @@ -992,11 +994,13 @@ Name | Type | Description | Notes **originalPath** | **String**| | [optional] **page** | **num**| | [optional] **personIds** | [**List**](String.md)| | [optional] [default to const []] + **previewPath** | **String**| | [optional] **resizePath** | **String**| | [optional] **size** | **num**| | [optional] **state** | **String**| | [optional] **takenAfter** | **DateTime**| | [optional] **takenBefore** | **DateTime**| | [optional] + **thumbnailPath** | **String**| | [optional] **trashedAfter** | **DateTime**| | [optional] **trashedBefore** | **DateTime**| | [optional] **type** | [**AssetTypeEnum**](.md)| | [optional] diff --git a/mobile/openapi/doc/ImageFormat.md b/mobile/openapi/doc/ImageFormat.md new file mode 100644 index 0000000000000..312e501c17f36 --- /dev/null +++ b/mobile/openapi/doc/ImageFormat.md @@ -0,0 +1,14 @@ +# openapi.model.ImageFormat + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MetadataSearchDto.md b/mobile/openapi/doc/MetadataSearchDto.md index d1d098fb0ec82..5dc50c00fad9d 100644 --- a/mobile/openapi/doc/MetadataSearchDto.md +++ b/mobile/openapi/doc/MetadataSearchDto.md @@ -35,11 +35,13 @@ Name | Type | Description | Notes **originalPath** | **String** | | [optional] **page** | **num** | | [optional] **personIds** | **List** | | [optional] [default to const []] +**previewPath** | **String** | | [optional] **resizePath** | **String** | | [optional] **size** | **num** | | [optional] **state** | **String** | | [optional] **takenAfter** | [**DateTime**](DateTime.md) | | [optional] **takenBefore** | [**DateTime**](DateTime.md) | | [optional] +**thumbnailPath** | **String** | | [optional] **trashedAfter** | [**DateTime**](DateTime.md) | | [optional] **trashedBefore** | [**DateTime**](DateTime.md) | | [optional] **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional] diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index ad1afbe9fcb05..fc3d3dd0b732e 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | | +**image** | [**SystemConfigImageDto**](SystemConfigImageDto.md) | | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | | **library_** | [**SystemConfigLibraryDto**](SystemConfigLibraryDto.md) | | **logging** | [**SystemConfigLoggingDto**](SystemConfigLoggingDto.md) | | @@ -21,7 +22,6 @@ Name | Type | Description | Notes **server** | [**SystemConfigServerDto**](SystemConfigServerDto.md) | | **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | | **theme** | [**SystemConfigThemeDto**](SystemConfigThemeDto.md) | | -**thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | | **trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.md) | | **user** | [**SystemConfigUserDto**](SystemConfigUserDto.md) | | diff --git a/mobile/openapi/doc/SystemConfigThumbnailDto.md b/mobile/openapi/doc/SystemConfigImageDto.md similarity index 65% rename from mobile/openapi/doc/SystemConfigThumbnailDto.md rename to mobile/openapi/doc/SystemConfigImageDto.md index 491bf9f120066..1b9bbe726dbe0 100644 --- a/mobile/openapi/doc/SystemConfigThumbnailDto.md +++ b/mobile/openapi/doc/SystemConfigImageDto.md @@ -1,4 +1,4 @@ -# openapi.model.SystemConfigThumbnailDto +# openapi.model.SystemConfigImageDto ## Load the model package ```dart @@ -9,9 +9,11 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **colorspace** | [**Colorspace**](Colorspace.md) | | -**jpegSize** | **int** | | +**previewFormat** | [**ImageFormat**](ImageFormat.md) | | +**previewSize** | **int** | | **quality** | **int** | | -**webpSize** | **int** | | +**thumbnailFormat** | [**ImageFormat**](ImageFormat.md) | | +**thumbnailSize** | **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) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 2abc20fde8117..1600dfb331809 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -112,6 +112,7 @@ part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; part 'model/file_report_fix_dto.dart'; part 'model/file_report_item_dto.dart'; +part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; @@ -176,6 +177,7 @@ part 'model/smart_info_response_dto.dart'; part 'model/smart_search_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; +part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; part 'model/system_config_library_scan_dto.dart'; @@ -191,7 +193,6 @@ part 'model/system_config_server_dto.dart'; part 'model/system_config_storage_template_dto.dart'; part 'model/system_config_template_storage_option_dto.dart'; part 'model/system_config_theme_dto.dart'; -part 'model/system_config_thumbnail_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/system_config_user_dto.dart'; part 'model/tag_response_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index e16ccc73e55a9..10f1c0c10e9a3 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -931,6 +931,8 @@ class AssetApi { /// /// * [List] personIds: /// + /// * [String] previewPath: + /// /// * [String] resizePath: /// /// * [num] size: @@ -941,6 +943,8 @@ class AssetApi { /// /// * [DateTime] takenBefore: /// + /// * [String] thumbnailPath: + /// /// * [DateTime] trashedAfter: /// /// * [DateTime] trashedBefore: @@ -962,7 +966,7 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? previewPath, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, String? thumbnailPath, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/assets'; @@ -1054,6 +1058,9 @@ class AssetApi { if (personIds != null) { queryParams.addAll(_queryParams('multi', 'personIds', personIds)); } + if (previewPath != null) { + queryParams.addAll(_queryParams('', 'previewPath', previewPath)); + } if (resizePath != null) { queryParams.addAll(_queryParams('', 'resizePath', resizePath)); } @@ -1069,6 +1076,9 @@ class AssetApi { if (takenBefore != null) { queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); } + if (thumbnailPath != null) { + queryParams.addAll(_queryParams('', 'thumbnailPath', thumbnailPath)); + } if (trashedAfter != null) { queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); } @@ -1173,6 +1183,8 @@ class AssetApi { /// /// * [List] personIds: /// + /// * [String] previewPath: + /// /// * [String] resizePath: /// /// * [num] size: @@ -1183,6 +1195,8 @@ class AssetApi { /// /// * [DateTime] takenBefore: /// + /// * [String] thumbnailPath: + /// /// * [DateTime] trashedAfter: /// /// * [DateTime] trashedBefore: @@ -1204,8 +1218,8 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { - final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, personIds: personIds, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); + Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? previewPath, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, String? thumbnailPath, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, personIds: personIds, previewPath: previewPath, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, thumbnailPath: thumbnailPath, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 312153788c5a0..4a145d0c4420c 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -304,6 +304,8 @@ class ApiClient { return FileReportFixDto.fromJson(value); case 'FileReportItemDto': return FileReportItemDto.fromJson(value); + case 'ImageFormat': + return ImageFormatTypeTransformer().decode(value); case 'JobCommand': return JobCommandTypeTransformer().decode(value); case 'JobCommandDto': @@ -432,6 +434,8 @@ class ApiClient { return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigImageDto': + return SystemConfigImageDto.fromJson(value); case 'SystemConfigJobDto': return SystemConfigJobDto.fromJson(value); case 'SystemConfigLibraryDto': @@ -462,8 +466,6 @@ class ApiClient { return SystemConfigTemplateStorageOptionDto.fromJson(value); case 'SystemConfigThemeDto': return SystemConfigThemeDto.fromJson(value); - case 'SystemConfigThumbnailDto': - return SystemConfigThumbnailDto.fromJson(value); case 'SystemConfigTrashDto': return SystemConfigTrashDto.fromJson(value); case 'SystemConfigUserDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index d186845d94dc3..9d2d86cba5a00 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -79,6 +79,9 @@ String parameterToString(dynamic value) { if (value is EntityType) { return EntityTypeTypeTransformer().encode(value).toString(); } + if (value is ImageFormat) { + return ImageFormatTypeTransformer().encode(value).toString(); + } if (value is JobCommand) { return JobCommandTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/image_format.dart b/mobile/openapi/lib/model/image_format.dart new file mode 100644 index 0000000000000..570b6ca6e6dac --- /dev/null +++ b/mobile/openapi/lib/model/image_format.dart @@ -0,0 +1,85 @@ +// +// 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 ImageFormat { + /// Instantiate a new enum with the provided [value]. + const ImageFormat._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const jpeg = ImageFormat._(r'jpeg'); + static const webp = ImageFormat._(r'webp'); + + /// List of all possible values in this [enum][ImageFormat]. + static const values = [ + jpeg, + webp, + ]; + + static ImageFormat? fromJson(dynamic value) => ImageFormatTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ImageFormat.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ImageFormat] to String, +/// and [decode] dynamic data back to [ImageFormat]. +class ImageFormatTypeTransformer { + factory ImageFormatTypeTransformer() => _instance ??= const ImageFormatTypeTransformer._(); + + const ImageFormatTypeTransformer._(); + + String encode(ImageFormat data) => data.value; + + /// Decodes a [dynamic value][data] to a ImageFormat. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ImageFormat? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'jpeg': return ImageFormat.jpeg; + case r'webp': return ImageFormat.webp; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ImageFormatTypeTransformer] instance. + static ImageFormatTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 86a2856e66762..3f770ed092f1d 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -40,11 +40,13 @@ class MetadataSearchDto { this.originalPath, this.page, this.personIds = const [], + this.previewPath, this.resizePath, this.size, this.state, this.takenAfter, this.takenBefore, + this.thumbnailPath, this.trashedAfter, this.trashedBefore, this.type, @@ -268,6 +270,14 @@ class MetadataSearchDto { List personIds; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? previewPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -308,6 +318,14 @@ class MetadataSearchDto { /// DateTime? takenBefore; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? thumbnailPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -419,11 +437,13 @@ class MetadataSearchDto { other.originalPath == originalPath && other.page == page && _deepEquality.equals(other.personIds, personIds) && + other.previewPath == previewPath && other.resizePath == resizePath && other.size == size && other.state == state && other.takenAfter == takenAfter && other.takenBefore == takenBefore && + other.thumbnailPath == thumbnailPath && other.trashedAfter == trashedAfter && other.trashedBefore == trashedBefore && other.type == type && @@ -466,11 +486,13 @@ class MetadataSearchDto { (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + (personIds.hashCode) + + (previewPath == null ? 0 : previewPath!.hashCode) + (resizePath == null ? 0 : resizePath!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + + (thumbnailPath == null ? 0 : thumbnailPath!.hashCode) + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + (trashedBefore == null ? 0 : trashedBefore!.hashCode) + (type == null ? 0 : type!.hashCode) + @@ -484,7 +506,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, resizePath=$resizePath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, webpPath=$webpPath, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, resizePath=$resizePath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, webpPath=$webpPath, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -619,6 +641,11 @@ class MetadataSearchDto { // json[r'page'] = null; } json[r'personIds'] = this.personIds; + if (this.previewPath != null) { + json[r'previewPath'] = this.previewPath; + } else { + // json[r'previewPath'] = null; + } if (this.resizePath != null) { json[r'resizePath'] = this.resizePath; } else { @@ -644,6 +671,11 @@ class MetadataSearchDto { } else { // json[r'takenBefore'] = null; } + if (this.thumbnailPath != null) { + json[r'thumbnailPath'] = this.thumbnailPath; + } else { + // json[r'thumbnailPath'] = null; + } if (this.trashedAfter != null) { json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); } else { @@ -735,11 +767,13 @@ class MetadataSearchDto { personIds: json[r'personIds'] is Iterable ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], + previewPath: mapValueOfType(json, r'previewPath'), resizePath: mapValueOfType(json, r'resizePath'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), takenAfter: mapDateTime(json, r'takenAfter', r''), takenBefore: mapDateTime(json, r'takenBefore', r''), + thumbnailPath: mapValueOfType(json, r'thumbnailPath'), trashedAfter: mapDateTime(json, r'trashedAfter', r''), trashedBefore: mapDateTime(json, r'trashedBefore', r''), type: AssetTypeEnum.fromJson(json[r'type']), diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart index ea722076d9568..11cdf41ea16f0 100644 --- a/mobile/openapi/lib/model/path_type.dart +++ b/mobile/openapi/lib/model/path_type.dart @@ -24,8 +24,8 @@ class PathType { String toJson() => value; static const original = PathType._(r'original'); - static const jpegThumbnail = PathType._(r'jpeg_thumbnail'); - static const webpThumbnail = PathType._(r'webp_thumbnail'); + static const preview = PathType._(r'preview'); + static const thumbnail = PathType._(r'thumbnail'); static const encodedVideo = PathType._(r'encoded_video'); static const sidecar = PathType._(r'sidecar'); static const face = PathType._(r'face'); @@ -34,8 +34,8 @@ class PathType { /// List of all possible values in this [enum][PathType]. static const values = [ original, - jpegThumbnail, - webpThumbnail, + preview, + thumbnail, encodedVideo, sidecar, face, @@ -79,8 +79,8 @@ class PathTypeTypeTransformer { if (data != null) { switch (data) { case r'original': return PathType.original; - case r'jpeg_thumbnail': return PathType.jpegThumbnail; - case r'webp_thumbnail': return PathType.webpThumbnail; + case r'preview': return PathType.preview; + case r'thumbnail': return PathType.thumbnail; case r'encoded_video': return PathType.encodedVideo; case r'sidecar': return PathType.sidecar; case r'face': return PathType.face; diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 0b5f64fc2709b..f075d37c8654e 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -14,6 +14,7 @@ class SystemConfigDto { /// Returns a new [SystemConfigDto] instance. SystemConfigDto({ required this.ffmpeg, + required this.image, required this.job, required this.library_, required this.logging, @@ -26,13 +27,14 @@ class SystemConfigDto { required this.server, required this.storageTemplate, required this.theme, - required this.thumbnail, required this.trash, required this.user, }); SystemConfigFFmpegDto ffmpeg; + SystemConfigImageDto image; + SystemConfigJobDto job; SystemConfigLibraryDto library_; @@ -57,8 +59,6 @@ class SystemConfigDto { SystemConfigThemeDto theme; - SystemConfigThumbnailDto thumbnail; - SystemConfigTrashDto trash; SystemConfigUserDto user; @@ -66,6 +66,7 @@ class SystemConfigDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && other.ffmpeg == ffmpeg && + other.image == image && other.job == job && other.library_ == library_ && other.logging == logging && @@ -78,7 +79,6 @@ class SystemConfigDto { other.server == server && other.storageTemplate == storageTemplate && other.theme == theme && - other.thumbnail == thumbnail && other.trash == trash && other.user == user; @@ -86,6 +86,7 @@ class SystemConfigDto { int get hashCode => // ignore: unnecessary_parenthesis (ffmpeg.hashCode) + + (image.hashCode) + (job.hashCode) + (library_.hashCode) + (logging.hashCode) + @@ -98,16 +99,16 @@ class SystemConfigDto { (server.hashCode) + (storageTemplate.hashCode) + (theme.hashCode) + - (thumbnail.hashCode) + (trash.hashCode) + (user.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; json[r'ffmpeg'] = this.ffmpeg; + json[r'image'] = this.image; json[r'job'] = this.job; json[r'library'] = this.library_; json[r'logging'] = this.logging; @@ -120,7 +121,6 @@ class SystemConfigDto { json[r'server'] = this.server; json[r'storageTemplate'] = this.storageTemplate; json[r'theme'] = this.theme; - json[r'thumbnail'] = this.thumbnail; json[r'trash'] = this.trash; json[r'user'] = this.user; return json; @@ -135,6 +135,7 @@ class SystemConfigDto { return SystemConfigDto( ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, + image: SystemConfigImageDto.fromJson(json[r'image'])!, job: SystemConfigJobDto.fromJson(json[r'job'])!, library_: SystemConfigLibraryDto.fromJson(json[r'library'])!, logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!, @@ -147,7 +148,6 @@ class SystemConfigDto { server: SystemConfigServerDto.fromJson(json[r'server'])!, storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, theme: SystemConfigThemeDto.fromJson(json[r'theme'])!, - thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!, trash: SystemConfigTrashDto.fromJson(json[r'trash'])!, user: SystemConfigUserDto.fromJson(json[r'user'])!, ); @@ -198,6 +198,7 @@ class SystemConfigDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'ffmpeg', + 'image', 'job', 'library', 'logging', @@ -210,7 +211,6 @@ class SystemConfigDto { 'server', 'storageTemplate', 'theme', - 'thumbnail', 'trash', 'user', }; diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart new file mode 100644 index 0000000000000..1c830861aff07 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -0,0 +1,138 @@ +// +// 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 SystemConfigImageDto { + /// Returns a new [SystemConfigImageDto] instance. + SystemConfigImageDto({ + required this.colorspace, + required this.previewFormat, + required this.previewSize, + required this.quality, + required this.thumbnailFormat, + required this.thumbnailSize, + }); + + Colorspace colorspace; + + ImageFormat previewFormat; + + int previewSize; + + int quality; + + ImageFormat thumbnailFormat; + + int thumbnailSize; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && + other.colorspace == colorspace && + other.previewFormat == previewFormat && + other.previewSize == previewSize && + other.quality == quality && + other.thumbnailFormat == thumbnailFormat && + other.thumbnailSize == thumbnailSize; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (colorspace.hashCode) + + (previewFormat.hashCode) + + (previewSize.hashCode) + + (quality.hashCode) + + (thumbnailFormat.hashCode) + + (thumbnailSize.hashCode); + + @override + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + + Map toJson() { + final json = {}; + json[r'colorspace'] = this.colorspace; + json[r'previewFormat'] = this.previewFormat; + json[r'previewSize'] = this.previewSize; + json[r'quality'] = this.quality; + json[r'thumbnailFormat'] = this.thumbnailFormat; + json[r'thumbnailSize'] = this.thumbnailSize; + return json; + } + + /// Returns a new [SystemConfigImageDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigImageDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigImageDto( + colorspace: Colorspace.fromJson(json[r'colorspace'])!, + previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, + previewSize: mapValueOfType(json, r'previewSize')!, + quality: mapValueOfType(json, r'quality')!, + thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!, + thumbnailSize: mapValueOfType(json, r'thumbnailSize')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigImageDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigImageDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigImageDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigImageDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'colorspace', + 'previewFormat', + 'previewSize', + 'quality', + 'thumbnailFormat', + 'thumbnailSize', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart b/mobile/openapi/lib/model/system_config_thumbnail_dto.dart deleted file mode 100644 index f1d55d6220aec..0000000000000 --- a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart +++ /dev/null @@ -1,122 +0,0 @@ -// -// 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.colorspace, - required this.jpegSize, - required this.quality, - required this.webpSize, - }); - - Colorspace colorspace; - - int jpegSize; - - int quality; - - int webpSize; - - @override - bool operator ==(Object other) => identical(this, other) || other is SystemConfigThumbnailDto && - other.colorspace == colorspace && - other.jpegSize == jpegSize && - other.quality == quality && - other.webpSize == webpSize; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (colorspace.hashCode) + - (jpegSize.hashCode) + - (quality.hashCode) + - (webpSize.hashCode); - - @override - String toString() => 'SystemConfigThumbnailDto[colorspace=$colorspace, jpegSize=$jpegSize, quality=$quality, webpSize=$webpSize]'; - - Map toJson() { - final json = {}; - json[r'colorspace'] = this.colorspace; - json[r'jpegSize'] = this.jpegSize; - json[r'quality'] = this.quality; - 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(); - - return SystemConfigThumbnailDto( - colorspace: Colorspace.fromJson(json[r'colorspace'])!, - jpegSize: mapValueOfType(json, r'jpegSize')!, - quality: mapValueOfType(json, r'quality')!, - webpSize: mapValueOfType(json, r'webpSize')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - 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 mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - 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 = { - 'colorspace', - 'jpegSize', - 'quality', - 'webpSize', - }; -} - diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 41d0ac8f5c57e..0c1729e95f7f5 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -100,7 +100,7 @@ void main() { // TODO }); - //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isNotInAlbum, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, List personIds, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isNotInAlbum, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, List personIds, String previewPath, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, String thumbnailPath, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async test('test searchAssets', () async { // TODO }); diff --git a/mobile/openapi/test/image_format_test.dart b/mobile/openapi/test/image_format_test.dart new file mode 100644 index 0000000000000..2bb1512a68249 --- /dev/null +++ b/mobile/openapi/test/image_format_test.dart @@ -0,0 +1,21 @@ +// +// 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 ImageFormat +void main() { + + group('test ImageFormat', () { + + }); + +} diff --git a/mobile/openapi/test/metadata_search_dto_test.dart b/mobile/openapi/test/metadata_search_dto_test.dart index f817b7da74fac..62979da9c0644 100644 --- a/mobile/openapi/test/metadata_search_dto_test.dart +++ b/mobile/openapi/test/metadata_search_dto_test.dart @@ -151,6 +151,11 @@ void main() { // TODO }); + // String previewPath + test('to test the property `previewPath`', () async { + // TODO + }); + // String resizePath test('to test the property `resizePath`', () async { // TODO @@ -176,6 +181,11 @@ void main() { // TODO }); + // String thumbnailPath + test('to test the property `thumbnailPath`', () async { + // TODO + }); + // DateTime trashedAfter test('to test the property `trashedAfter`', () async { // TODO diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index b41d07e5f9b28..e88ee17c4b55e 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // SystemConfigImageDto image + test('to test the property `image`', () async { + // TODO + }); + // SystemConfigJobDto job test('to test the property `job`', () async { // TODO @@ -81,11 +86,6 @@ void main() { // TODO }); - // SystemConfigThumbnailDto thumbnail - test('to test the property `thumbnail`', () async { - // TODO - }); - // SystemConfigTrashDto trash test('to test the property `trash`', () async { // TODO diff --git a/mobile/openapi/test/system_config_thumbnail_dto_test.dart b/mobile/openapi/test/system_config_image_dto_test.dart similarity index 53% rename from mobile/openapi/test/system_config_thumbnail_dto_test.dart rename to mobile/openapi/test/system_config_image_dto_test.dart index 3cc66f4677843..aef907bbe694d 100644 --- a/mobile/openapi/test/system_config_thumbnail_dto_test.dart +++ b/mobile/openapi/test/system_config_image_dto_test.dart @@ -11,18 +11,23 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for SystemConfigThumbnailDto +// tests for SystemConfigImageDto void main() { - // final instance = SystemConfigThumbnailDto(); + // final instance = SystemConfigImageDto(); - group('test SystemConfigThumbnailDto', () { + group('test SystemConfigImageDto', () { // Colorspace colorspace test('to test the property `colorspace`', () async { // TODO }); - // int jpegSize - test('to test the property `jpegSize`', () async { + // ImageFormat previewFormat + test('to test the property `previewFormat`', () async { + // TODO + }); + + // int previewSize + test('to test the property `previewSize`', () async { // TODO }); @@ -31,8 +36,13 @@ void main() { // TODO }); - // int webpSize - test('to test the property `webpSize`', () async { + // ImageFormat thumbnailFormat + test('to test the property `thumbnailFormat`', () async { + // TODO + }); + + // int thumbnailSize + test('to test the property `thumbnailSize`', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a2aae92c41bca..16a9ad63afcd3 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2101,10 +2101,19 @@ } } }, + { + "name": "previewPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "resizePath", "required": false, "in": "query", + "deprecated": true, "schema": { "type": "string" } @@ -2143,6 +2152,14 @@ "type": "string" } }, + { + "name": "thumbnailPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "trashedAfter", "required": false, @@ -2191,6 +2208,7 @@ "name": "webpPath", "required": false, "in": "query", + "deprecated": true, "schema": { "type": "string" } @@ -8114,6 +8132,13 @@ ], "type": "object" }, + "ImageFormat": { + "enum": [ + "jpeg", + "webp" + ], + "type": "string" + }, "JobCommand": { "enum": [ "start", @@ -8555,7 +8580,11 @@ }, "type": "array" }, + "previewPath": { + "type": "string" + }, "resizePath": { + "deprecated": true, "type": "string" }, "size": { @@ -8572,6 +8601,9 @@ "format": "date-time", "type": "string" }, + "thumbnailPath": { + "type": "string" + }, "trashedAfter": { "format": "date-time", "type": "string" @@ -8592,6 +8624,7 @@ "type": "string" }, "webpPath": { + "deprecated": true, "type": "string" }, "withArchived": { @@ -8746,8 +8779,8 @@ "PathType": { "enum": [ "original", - "jpeg_thumbnail", - "webp_thumbnail", + "preview", + "thumbnail", "encoded_video", "sidecar", "face", @@ -9743,6 +9776,9 @@ "ffmpeg": { "$ref": "#/components/schemas/SystemConfigFFmpegDto" }, + "image": { + "$ref": "#/components/schemas/SystemConfigImageDto" + }, "job": { "$ref": "#/components/schemas/SystemConfigJobDto" }, @@ -9779,9 +9815,6 @@ "theme": { "$ref": "#/components/schemas/SystemConfigThemeDto" }, - "thumbnail": { - "$ref": "#/components/schemas/SystemConfigThumbnailDto" - }, "trash": { "$ref": "#/components/schemas/SystemConfigTrashDto" }, @@ -9791,6 +9824,7 @@ }, "required": [ "ffmpeg", + "image", "job", "library", "logging", @@ -9803,7 +9837,6 @@ "server", "storageTemplate", "theme", - "thumbnail", "trash", "user" ], @@ -9902,6 +9935,37 @@ ], "type": "object" }, + "SystemConfigImageDto": { + "properties": { + "colorspace": { + "$ref": "#/components/schemas/Colorspace" + }, + "previewFormat": { + "$ref": "#/components/schemas/ImageFormat" + }, + "previewSize": { + "type": "integer" + }, + "quality": { + "type": "integer" + }, + "thumbnailFormat": { + "$ref": "#/components/schemas/ImageFormat" + }, + "thumbnailSize": { + "type": "integer" + } + }, + "required": [ + "colorspace", + "previewFormat", + "previewSize", + "quality", + "thumbnailFormat", + "thumbnailSize" + ], + "type": "object" + }, "SystemConfigJobDto": { "properties": { "backgroundTask": { @@ -10251,29 +10315,6 @@ ], "type": "object" }, - "SystemConfigThumbnailDto": { - "properties": { - "colorspace": { - "$ref": "#/components/schemas/Colorspace" - }, - "jpegSize": { - "type": "integer" - }, - "quality": { - "type": "integer" - }, - "webpSize": { - "type": "integer" - } - }, - "required": [ - "colorspace", - "jpegSize", - "quality", - "webpSize" - ], - "type": "object" - }, "SystemConfigTrashDto": { "properties": { "days": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index b9b4978a9bf9a..e63ccb4d641aa 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -640,11 +640,13 @@ export type MetadataSearchDto = { originalPath?: string; page?: number; personIds?: string[]; + previewPath?: string; resizePath?: string; size?: number; state?: string; takenAfter?: string; takenBefore?: string; + thumbnailPath?: string; trashedAfter?: string; trashedBefore?: string; "type"?: AssetTypeEnum; @@ -827,6 +829,14 @@ export type SystemConfigFFmpegDto = { transcode: TranscodePolicy; twoPass: boolean; }; +export type SystemConfigImageDto = { + colorspace: Colorspace; + previewFormat: ImageFormat; + previewSize: number; + quality: number; + thumbnailFormat: ImageFormat; + thumbnailSize: number; +}; export type JobSettingsDto = { concurrency: number; }; @@ -919,12 +929,6 @@ export type SystemConfigStorageTemplateDto = { export type SystemConfigThemeDto = { customCss: string; }; -export type SystemConfigThumbnailDto = { - colorspace: Colorspace; - jpegSize: number; - quality: number; - webpSize: number; -}; export type SystemConfigTrashDto = { days: number; enabled: boolean; @@ -934,6 +938,7 @@ export type SystemConfigUserDto = { }; export type SystemConfigDto = { ffmpeg: SystemConfigFFmpegDto; + image: SystemConfigImageDto; job: SystemConfigJobDto; library: SystemConfigLibraryDto; logging: SystemConfigLoggingDto; @@ -946,7 +951,6 @@ export type SystemConfigDto = { server: SystemConfigServerDto; storageTemplate: SystemConfigStorageTemplateDto; theme: SystemConfigThemeDto; - thumbnail: SystemConfigThumbnailDto; trash: SystemConfigTrashDto; user: SystemConfigUserDto; }; @@ -1497,7 +1501,7 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } -export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { +export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { checksum?: string; city?: string; country?: string; @@ -1525,11 +1529,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef originalPath?: string; page?: number; personIds?: string[]; + previewPath?: string; resizePath?: string; size?: number; state?: string; takenAfter?: string; takenBefore?: string; + thumbnailPath?: string; trashedAfter?: string; trashedBefore?: string; $type?: AssetTypeEnum; @@ -1573,11 +1579,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef originalPath, page, personIds, + previewPath, resizePath, size, state, takenAfter, takenBefore, + thumbnailPath, trashedAfter, trashedBefore, "type": $type, @@ -2802,8 +2810,8 @@ export enum PathEntityType { } export enum PathType { Original = "original", - JpegThumbnail = "jpeg_thumbnail", - WebpThumbnail = "webp_thumbnail", + Preview = "preview", + Thumbnail = "thumbnail", EncodedVideo = "encoded_video", Sidecar = "sidecar", Face = "face", @@ -2885,6 +2893,14 @@ export enum TranscodePolicy { Required = "required", Disabled = "disabled" } +export enum Colorspace { + Srgb = "srgb", + P3 = "p3" +} +export enum ImageFormat { + Jpeg = "jpeg", + Webp = "webp" +} export enum LogLevel { Verbose = "verbose", Debug = "debug", @@ -2901,10 +2917,6 @@ export enum ModelType { FacialRecognition = "facial-recognition", Clip = "clip" } -export enum Colorspace { - Srgb = "srgb", - P3 = "p3" -} export enum MapTheme { Light = "light", Dark = "dark" diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index ee9f12e5183b1..035f90c911c89 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -4,6 +4,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { ImageFormat } from 'src/entities/system-config.entity'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; @@ -34,7 +35,8 @@ export interface MoveRequest { }; } -type GeneratedAssetPath = AssetPathType.JPEG_THUMBNAIL | AssetPathType.WEBP_THUMBNAIL | AssetPathType.ENCODED_VIDEO; +export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL; +export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; let instance: StorageCore | null; @@ -94,12 +96,8 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); } - static getLargeThumbnailPath(asset: AssetEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.jpeg`); - } - - static getSmallThumbnailPath(asset: AssetEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.webp`); + static getImagePath(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { + return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); } static getEncodedVideoPath(asset: AssetEntity) { @@ -128,34 +126,23 @@ export class StorageCore { return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR); } - async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { - const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; - switch (pathType) { - case AssetPathType.JPEG_THUMBNAIL: { - return this.moveFile({ - entityId, - pathType, - oldPath: resizePath, - newPath: StorageCore.getLargeThumbnailPath(asset), - }); - } - case AssetPathType.WEBP_THUMBNAIL: { - return this.moveFile({ - entityId, - pathType, - oldPath: webpPath, - newPath: StorageCore.getSmallThumbnailPath(asset), - }); - } - case AssetPathType.ENCODED_VIDEO: { - return this.moveFile({ - entityId, - pathType, - oldPath: encodedVideoPath, - newPath: StorageCore.getEncodedVideoPath(asset), - }); - } - } + async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { + const { id: entityId, previewPath, thumbnailPath } = asset; + return this.moveFile({ + entityId, + pathType, + oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, + newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), + }); + } + + async moveAssetVideo(asset: AssetEntity) { + return this.moveFile({ + entityId: asset.id, + pathType: AssetPathType.ENCODED_VIDEO, + oldPath: asset.encodedVideoPath, + newPath: StorageCore.getEncodedVideoPath(asset), + }); } async movePersonFile(person: PersonEntity, pathType: PersonPathType) { @@ -294,11 +281,11 @@ export class StorageCore { case AssetPathType.ORIGINAL: { return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.JPEG_THUMBNAIL: { - return this.assetRepository.update({ id, resizePath: newPath }); + case AssetPathType.PREVIEW: { + return this.assetRepository.update({ id, previewPath: newPath }); } - case AssetPathType.WEBP_THUMBNAIL: { - return this.assetRepository.update({ id, webpPath: newPath }); + case AssetPathType.THUMBNAIL: { + return this.assetRepository.update({ id, thumbnailPath: newPath }); } case AssetPathType.ENCODED_VIDEO: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 01bfacc9bd4ed..3a1ea47bbe4ac 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -10,6 +10,7 @@ import { AudioCodec, CQMode, Colorspace, + ImageFormat, LogLevel, SystemConfig, SystemConfigEntity, @@ -112,9 +113,11 @@ export const defaults = Object.freeze({ hashVerificationEnabled: true, template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, - thumbnail: { - webpSize: 250, - jpegSize: 1440, + image: { + thumbnailFormat: ImageFormat.WEBP, + thumbnailSize: 250, + previewFormat: ImageFormat.JPEG, + previewSize: 1440, quality: 80, colorspace: Colorspace.P3, }, diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index cf16a99a29cdb..bdda36d15e734 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -82,7 +82,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As type: entity.type, thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!entity.resizePath, + resized: !!entity.previewPath, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -100,7 +100,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As type: entity.type, originalPath: entity.originalPath, originalFileName: entity.originalFileName, - resized: !!entity.resizePath, + resized: !!entity.previewPath, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 799baddee386c..d96ce0d98a0a0 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -163,13 +163,25 @@ export class MetadataSearchDto extends BaseSearchDto { @IsString() @IsNotEmpty() @Optional() + @ApiProperty({ deprecated: true }) resizePath?: string; @IsString() @IsNotEmpty() @Optional() + @ApiProperty({ deprecated: true }) webpPath?: string; + @IsString() + @IsNotEmpty() + @Optional() + previewPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + thumbnailPath?: string; + @IsString() @IsNotEmpty() @Optional() diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 740f1672ee8b4..9f80e8d6a3b3c 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -22,6 +22,7 @@ import { AudioCodec, CQMode, Colorspace, + ImageFormat, LogLevel, SystemConfig, ToneMapping, @@ -385,18 +386,26 @@ export class SystemConfigThemeDto { customCss!: string; } -class SystemConfigThumbnailDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - webpSize!: number; +class SystemConfigImageDto { + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + thumbnailFormat!: ImageFormat; @IsInt() @Min(1) @Type(() => Number) @ApiProperty({ type: 'integer' }) - jpegSize!: number; + thumbnailSize!: number; + + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + previewFormat!: ImageFormat; + + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + previewSize!: number; @IsInt() @Min(1) @@ -480,10 +489,10 @@ export class SystemConfigDto implements SystemConfig { @IsObject() job!: SystemConfigJobDto; - @Type(() => SystemConfigThumbnailDto) + @Type(() => SystemConfigImageDto) @ValidateNested() @IsObject() - thumbnail!: SystemConfigThumbnailDto; + image!: SystemConfigImageDto; @Type(() => SystemConfigTrashDto) @ValidateNested() diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 9166361153d56..c977560beac72 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -67,10 +67,10 @@ export class AssetEntity { originalPath!: string; @Column({ type: 'varchar', nullable: true }) - resizePath!: string | null; + previewPath!: string | null; @Column({ type: 'varchar', nullable: true, default: '' }) - webpPath!: string | null; + thumbnailPath!: string | null; @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts index de20cb9737053..f3dad6b280306 100644 --- a/server/src/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -24,8 +24,8 @@ export class MoveEntity { export enum AssetPathType { ORIGINAL = 'original', - JPEG_THUMBNAIL = 'jpeg_thumbnail', - WEBP_THUMBNAIL = 'webp_thumbnail', + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded_video', SIDECAR = 'sidecar', } diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts index 98b882a36e790..e07b6d4a2242f 100644 --- a/server/src/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -165,6 +165,11 @@ export enum Colorspace { P3 = 'p3', } +export enum ImageFormat { + JPEG = 'jpeg', + WEBP = 'webp', +} + export enum LogLevel { VERBOSE = 'verbose', DEBUG = 'debug', @@ -249,9 +254,11 @@ export interface SystemConfig { hashVerificationEnabled: boolean; template: string; }; - thumbnail: { - webpSize: number; - jpegSize: number; + image: { + thumbnailFormat: ImageFormat; + thumbnailSize: number; + previewFormat: ImageFormat; + previewSize: number; quality: number; colorspace: Colorspace; }; diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index 6f07fc752524b..eddaefcf383f4 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -33,9 +33,9 @@ export enum JobName { // thumbnails QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', - GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', - GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', - GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail', + GENERATE_PREVIEW = 'generate-preview', + GENERATE_THUMBNAIL = 'generate-thumbnail', + GENERATE_THUMBHASH = 'generate-thumbhash', GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', // metadata @@ -160,9 +160,9 @@ export type JobItem = // Thumbnails | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob } - | { name: JobName.GENERATE_JPEG_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob } + | { name: JobName.GENERATE_PREVIEW; data: IEntityJob } + | { name: JobName.GENERATE_THUMBNAIL; data: IEntityJob } + | { name: JobName.GENERATE_THUMBHASH; data: IEntityJob } // User | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 0e34227d33076..5e51e94a5260c 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -1,11 +1,11 @@ import { Writable } from 'node:stream'; -import { TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; +import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; export const IMediaRepository = 'IMediaRepository'; export interface ResizeOptions { size: number; - format: 'webp' | 'jpeg'; + format: ImageFormat; colorspace: string; quality: number; } diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 1287202ade6cd..771b23e9c942d 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -117,8 +117,8 @@ export interface SearchPathOptions { encodedVideoPath?: string; originalFileName?: string; originalPath?: string; - resizePath?: string; - webpPath?: string; + previewPath?: string; + thumbnailPath?: string; } export interface SearchExifOptions { diff --git a/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts b/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts new file mode 100644 index 0000000000000..ab6f2a4e9fc65 --- /dev/null +++ b/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameWebpJpegPaths1711257900274 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.renameColumn('assets', 'webpPath', 'thumbnailPath'); + await queryRunner.renameColumn('assets', 'resizePath', 'previewPath'); + await queryRunner.query(` + UPDATE system_config + SET key = 'image.previewSize' + WHERE key = 'thumbnail.jpegSize'`); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.thumbnailSize' + WHERE key = 'thumbnail.webpSize'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.quality' + WHERE key = 'thumbnail.quality'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.colorspace' + WHERE key = 'thumbnail.colorspace'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.renameColumn('assets', 'thumbnailPath', 'webpPath'); + await queryRunner.renameColumn('assets', 'previewPath', 'resizePath'); + await queryRunner.query(` + UPDATE system_config + SET key = 'thumbnail.jpegSize' + WHERE key = 'image.previewSize'`); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.webpSize' + WHERE key = 'image.thumbnailSize'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.quality' + WHERE key = 'image.quality'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.colorspace' + WHERE key = 'image.colorspace'`, + ); + } +} diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index fab0f5376f299..6f0e8cd5e3397 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -9,8 +9,8 @@ SELECT "entity"."deviceId" AS "entity_deviceId", "entity"."type" AS "entity_type", "entity"."originalPath" AS "entity_originalPath", - "entity"."resizePath" AS "entity_resizePath", - "entity"."webpPath" AS "entity_webpPath", + "entity"."previewPath" AS "entity_previewPath", + "entity"."thumbnailPath" AS "entity_thumbnailPath", "entity"."thumbhash" AS "entity_thumbhash", "entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."createdAt" AS "entity_createdAt", @@ -67,7 +67,7 @@ WHERE "entity"."ownerId" IN ($1) AND "entity"."isVisible" = true AND "entity"."isArchived" = false - AND "entity"."resizePath" IS NOT NULL + AND "entity"."previewPath" IS NOT NULL AND EXTRACT( DAY FROM @@ -92,8 +92,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -128,8 +128,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -213,8 +213,8 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."resizePath" AS "bd93d5747511a4dad4923546c51365bf1a803774_resizePath", - "bd93d5747511a4dad4923546c51365bf1a803774"."webpPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_webpPath", + "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath", + "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", @@ -294,8 +294,8 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -391,8 +391,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -437,8 +437,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -481,8 +481,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -570,8 +570,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -629,8 +629,8 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index b6a513ff94f7b..1cde746d8b1bf 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -152,8 +152,8 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", - "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", @@ -250,8 +250,8 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -380,8 +380,8 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", - "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index e985a1a6d7426..3e83d72384c8d 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -14,8 +14,8 @@ FROM "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -45,8 +45,8 @@ FROM "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -110,8 +110,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -141,8 +141,8 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -320,8 +320,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 27531cfc9ee08..78581b8ba13c6 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -28,8 +28,8 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", - "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", @@ -95,8 +95,8 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."resizePath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_resizePath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."webpPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_webpPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", @@ -218,8 +218,8 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", - "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", diff --git a/server/src/repositories/asset-v1.repository.ts b/server/src/repositories/asset-v1.repository.ts index 6f53d820c17ab..229e700fd5520 100644 --- a/server/src/repositories/asset-v1.repository.ts +++ b/server/src/repositories/asset-v1.repository.ts @@ -66,7 +66,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { getDetectedObjectsByUserId(userId: string): Promise { return this.assetRepository.query( ` - SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" + SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."previewPath" AS "resizePath", a."deviceAssetId", a."deviceId" FROM assets a LEFT JOIN smart_info si ON a.id = si."assetId" WHERE a."ownerId" = $1 @@ -80,7 +80,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { getLocationsByUserId(userId: string): Promise { return this.assetRepository.query( ` - SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" + SELECT DISTINCT ON (e.city) a.id, e.city, a."previewPath" AS "resizePath", a."deviceAssetId", a."deviceId" FROM assets a LEFT JOIN exif e ON a.id = e."assetId" WHERE a."ownerId" = $1 diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 907cddb893f23..e6389c2e565bb 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -83,7 +83,7 @@ export class AssetRepository implements IAssetRepository { `entity.ownerId IN (:...ownerIds) AND entity.isVisible = true AND entity.isArchived = false - AND entity.resizePath IS NOT NULL + AND entity.previewPath IS NOT NULL AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, { @@ -302,10 +302,10 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { where = [ - { resizePath: IsNull(), isVisible: true }, - { resizePath: '', isVisible: true }, - { webpPath: IsNull(), isVisible: true }, - { webpPath: '', isVisible: true }, + { previewPath: IsNull(), isVisible: true }, + { previewPath: '', isVisible: true }, + { thumbnailPath: IsNull(), isVisible: true }, + { thumbnailPath: '', isVisible: true }, { thumbhash: IsNull(), isVisible: true }, ]; break; @@ -339,7 +339,7 @@ export class AssetRepository implements IAssetRepository { }; where = { isVisible: true, - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), smartSearch: { embedding: IsNull(), }, @@ -352,7 +352,7 @@ export class AssetRepository implements IAssetRepository { smartInfo: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, smartInfo: { tags: IsNull(), @@ -367,7 +367,7 @@ export class AssetRepository implements IAssetRepository { jobStatus: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, faces: { assetId: IsNull(), @@ -385,7 +385,7 @@ export class AssetRepository implements IAssetRepository { faces: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, faces: { assetId: Not(IsNull()), diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index b55996ed05347..a7c99f93cbceb 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -35,9 +35,9 @@ export const JOBS_TO_QUEUE: Record = { // thumbnails [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_PREVIEW]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, // metadata diff --git a/server/src/services/asset-v1.service.spec.ts b/server/src/services/asset-v1.service.spec.ts index 898fb5a99f75c..735ac8322af09 100644 --- a/server/src/services/asset-v1.service.spec.ts +++ b/server/src/services/asset-v1.service.spec.ts @@ -44,13 +44,13 @@ const _getAsset_1 = () => { asset_1.deviceId = 'device_id_1'; asset_1.type = AssetType.VIDEO; asset_1.originalPath = 'fake_path/asset_1.jpeg'; - asset_1.resizePath = ''; + asset_1.previewPath = ''; asset_1.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.isFavorite = false; asset_1.isArchived = false; - asset_1.webpPath = ''; + asset_1.thumbnailPath = ''; asset_1.encodedVideoPath = ''; asset_1.duration = '0:00:00.000000'; asset_1.exifInfo = new ExifEntity(); diff --git a/server/src/services/asset-v1.service.ts b/server/src/services/asset-v1.service.ts index a24ddbd69dd93..97aa99d91d2dc 100644 --- a/server/src/services/asset-v1.service.ts +++ b/server/src/services/asset-v1.service.ts @@ -247,16 +247,16 @@ export class AssetServiceV1 { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { case GetAssetThumbnailFormatEnum.WEBP: { - if (asset.webpPath) { - return asset.webpPath; + if (asset.thumbnailPath) { + return asset.thumbnailPath; } this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); } case GetAssetThumbnailFormatEnum.JPEG: { - if (!asset.resizePath) { + if (!asset.previewPath) { throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); } - return asset.resizePath; + return asset.previewPath; } } } @@ -268,12 +268,12 @@ export class AssetServiceV1 { * Serve file viewer on the web */ if (dto.isWeb && mimeType != 'image/gif') { - if (!asset.resizePath) { + if (!asset.previewPath) { this.logger.error('Error serving IMAGE asset for web'); throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile'); } - return asset.resizePath; + return asset.previewPath; } /** @@ -283,15 +283,15 @@ export class AssetServiceV1 { return asset.originalPath; } - if (asset.webpPath && asset.webpPath.length > 0) { - return asset.webpPath; + if (asset.thumbnailPath && asset.thumbnailPath.length > 0) { + return asset.thumbnailPath; } - if (!asset.resizePath) { - throw new Error('resizePath not set'); + if (!asset.previewPath) { + throw new Error('previewPath not set'); } - return asset.resizePath; + return asset.previewPath; } private async getLibraryId(auth: AuthDto, libraryId?: string) { diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 683e2f5ae71e3..b90c194aa0ed8 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -661,8 +661,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetWithFace.webpPath, - assetWithFace.resizePath, + assetWithFace.thumbnailPath, + assetWithFace.previewPath, assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, @@ -745,8 +745,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetStub.external.webpPath, - assetStub.external.resizePath, + assetStub.external.thumbnailPath, + assetStub.external.previewPath, assetStub.external.encodedVideoPath, assetStub.external.sidecarPath, ], @@ -828,9 +828,7 @@ describe(AssetService.name, () => { it('should run the refresh thumbnails job', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }), - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, - ]); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]); }); it('should run the transcode video', async () => { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 556883afd4d11..135377e0bdb54 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -399,7 +399,7 @@ export class AssetService { await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } }); } - const files = [asset.webpPath, asset.resizePath, asset.encodedVideoPath, asset.sidecarPath]; + const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath, asset.sidecarPath]; if (!fromExternal) { files.push(asset.originalPath); } @@ -472,7 +472,7 @@ export class AssetService { } case AssetJobName.REGENERATE_THUMBNAIL: { - jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } }); + jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id } }); break; } diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index ff5e0d9c79eba..d40167429f75a 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -95,13 +95,13 @@ export class AuditService { break; } - case AssetPathType.JPEG_THUMBNAIL: { - await this.assetRepository.update({ id, resizePath: pathValue }); + case AssetPathType.PREVIEW: { + await this.assetRepository.update({ id, previewPath: pathValue }); break; } - case AssetPathType.WEBP_THUMBNAIL: { - await this.assetRepository.update({ id, webpPath: pathValue }); + case AssetPathType.THUMBNAIL: { + await this.assetRepository.update({ id, thumbnailPath: pathValue }); break; } @@ -174,8 +174,8 @@ export class AuditService { const orphans: FileReportItemDto[] = []; for await (const assets of pagination) { assetCount += assets.length; - for (const { id, originalPath, resizePath, encodedVideoPath, webpPath, isExternal, checksum } of assets) { - for (const file of [originalPath, resizePath, encodedVideoPath, webpPath]) { + for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { + for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { track(file); } @@ -191,14 +191,14 @@ export class AuditService { ) { orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); } - if (resizePath && !hasFile(thumbFiles, resizePath)) { - orphans.push({ ...entity, pathType: AssetPathType.JPEG_THUMBNAIL, pathValue: resizePath }); + if (previewPath && !hasFile(thumbFiles, previewPath)) { + orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); } - if (webpPath && !hasFile(thumbFiles, webpPath)) { - orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: webpPath }); + if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); } if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { - orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: encodedVideoPath }); + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); } } } diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index aa5739878ba66..ac0e502ae890e 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -279,7 +279,7 @@ describe(JobService.name, () => { }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.GENERATE_JPEG_THUMBNAIL], + jobs: [JobName.GENERATE_PREVIEW], }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } }, @@ -290,24 +290,24 @@ describe(JobService.name, () => { jobs: [], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, - jobs: [JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL], + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }, + jobs: [JobName.GENERATE_THUMBNAIL, JobName.GENERATE_THUMBHASH], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1', source: 'upload' } }, + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1', source: 'upload' } }, jobs: [ - JobName.GENERATE_WEBP_THUMBNAIL, - JobName.GENERATE_THUMBHASH_THUMBNAIL, + JobName.GENERATE_THUMBNAIL, + JobName.GENERATE_THUMBHASH, JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, ], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-live-image', source: 'upload' } }, + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-live-image', source: 'upload' } }, jobs: [ - JobName.GENERATE_WEBP_THUMBNAIL, - JobName.GENERATE_THUMBHASH_THUMBNAIL, + JobName.GENERATE_THUMBNAIL, + JobName.GENERATE_THUMBHASH, JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, @@ -329,7 +329,7 @@ describe(JobService.name, () => { for (const { item, jobs } of tests) { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { - if (item.name === JobName.GENERATE_JPEG_THUMBNAIL && item.data.source === 'upload') { + if (item.name === JobName.GENERATE_PREVIEW && item.data.source === 'upload') { if (item.data.id === 'asset-live-image') { assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); } else { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index c03b7c7bc26fa..3f9cd8a221d54 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -245,7 +245,7 @@ export class JobService { case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { if (item.data.source === 'upload') { - await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: item.data }); + await this.jobRepository.queue({ name: JobName.GENERATE_PREVIEW, data: item.data }); } break; } @@ -259,10 +259,10 @@ export class JobService { break; } - case JobName.GENERATE_JPEG_THUMBNAIL: { + case JobName.GENERATE_PREVIEW: { const jobs: JobItem[] = [ - { name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data }, - { name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: item.data }, + { name: JobName.GENERATE_THUMBNAIL, data: item.data }, + { name: JobName.GENERATE_THUMBHASH, data: item.data }, ]; if (item.data.source === 'upload') { @@ -282,7 +282,7 @@ export class JobService { break; } - case JobName.GENERATE_WEBP_THUMBNAIL: { + case JobName.GENERATE_THUMBNAIL: { if (item.data.source !== 'upload') { break; } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 4397730ab659f..3a650430ef59e 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -4,6 +4,7 @@ import { ExifEntity } from 'src/entities/exif.entity'; import { AudioCodec, Colorspace, + ImageFormat, SystemConfigKey, ToneMapping, TranscodeHWAccel, @@ -78,7 +79,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_JPEG_THUMBNAIL, + name: JobName.GENERATE_PREVIEW, data: { id: assetStub.image.id }, }, ]); @@ -136,7 +137,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_JPEG_THUMBNAIL, + name: JobName.GENERATE_PREVIEW, data: { id: assetStub.image.id }, }, ]); @@ -160,7 +161,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_WEBP_THUMBNAIL, + name: JobName.GENERATE_THUMBNAIL, data: { id: assetStub.image.id }, }, ]); @@ -184,7 +185,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBHASH_THUMBNAIL, + name: JobName.GENERATE_THUMBHASH, data: { id: assetStub.image.id }, }, ]); @@ -193,10 +194,10 @@ describe(MediaService.name, () => { }); }); - describe('handleGenerateJpegThumbnail', () => { + describe('handleGeneratePreview', () => { it('should skip thumbnail generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); @@ -204,25 +205,29 @@ describe(MediaService.name, () => { it('should skip video thumbnail generation if no video stream', async () => { mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should generate a thumbnail for an image', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', { - size: 1440, - format: 'jpeg', - quality: 80, - colorspace: Colorspace.SRGB, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + colorspace: Colorspace.SRGB, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -230,30 +235,34 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([ { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, ]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', { - size: 1440, - format: 'jpeg', - quality: 80, - colorspace: Colorspace.P3, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + colorspace: Colorspace.P3, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); it('should generate a thumbnail for a video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -266,19 +275,19 @@ describe(MediaService.name, () => { ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); it('should tonemap thumbnail for hdr video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -291,7 +300,7 @@ describe(MediaService.name, () => { ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -302,11 +311,11 @@ describe(MediaService.name, () => { { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '5000k' }, ]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -321,31 +330,35 @@ describe(MediaService.name, () => { it('should run successfully', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); }); }); - describe('handleGenerateWebpThumbnail', () => { + describe('handleGenerateThumbnail', () => { it('should skip thumbnail generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should generate a thumbnail', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', { - format: 'webp', - size: 250, - quality: 80, - colorspace: Colorspace.SRGB, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.SRGB, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp', + thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); }); @@ -354,31 +367,35 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([ { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, ]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', { - format: 'webp', - size: 250, - quality: 80, - colorspace: Colorspace.P3, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp', + thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); describe('handleGenerateThumbhashThumbnail', () => { it('should skip thumbhash generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbhash({ id: assetStub.image.id }); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); it('should skip thumbhash generation if resize path is missing', async () => { assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.noResizePath.id }); + await sut.handleGenerateThumbhash({ id: assetStub.noResizePath.id }); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); @@ -387,7 +404,7 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbhash({ id: assetStub.image.id }); expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 26aa2dce9f3f6..c56fd26e6a8d6 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; @@ -7,6 +7,7 @@ import { AssetPathType } from 'src/entities/move.entity'; import { AudioCodec, Colorspace, + ImageFormat, TranscodeHWAccel, TranscodePolicy, TranscodeTarget, @@ -81,15 +82,15 @@ export class MediaService { const jobs: JobItem[] = []; for (const asset of assets) { - if (!asset.resizePath || force) { - jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: asset.id } }); + if (!asset.previewPath || force) { + jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); continue; } - if (!asset.webpPath) { - jobs.push({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { id: asset.id } }); + if (!asset.thumbnailPath) { + jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); } if (!asset.thumbhash) { - jobs.push({ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: { id: asset.id } }); + jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); } } @@ -152,41 +153,41 @@ export class MediaService { } async handleAssetMigration({ id }: IEntityJob): Promise { + const { image } = await this.configCore.getConfig(); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return JobStatus.FAILED; } - await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL); - await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL); - await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO); + await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat); + await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); + await this.storageCore.moveAssetVideo(asset); return JobStatus.SUCCESS; } - async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise { + async handleGeneratePreview({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { return JobStatus.FAILED; } - const resizePath = await this.generateThumbnail(asset, 'jpeg'); - await this.assetRepository.update({ id: asset.id, resizePath }); + const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG); + await this.assetRepository.update({ id: asset.id, previewPath }); return JobStatus.SUCCESS; } - private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') { - const { thumbnail, ffmpeg } = await this.configCore.getConfig(); - const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize; - const path = - format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset); + private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { + const { image, ffmpeg } = await this.configCore.getConfig(); + const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize; + const path = StorageCore.getImagePath(asset, type, format); this.storageCore.ensureFolders(path); switch (asset.type) { case AssetType.IMAGE: { - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace; - const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality }; - await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions); + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const imageOptions = { format, size, colorspace, quality: image.quality }; + await this.mediaRepository.resize(asset.originalPath, path, imageOptions); break; } @@ -214,24 +215,24 @@ export class MediaService { return path; } - async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbnail({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { return JobStatus.FAILED; } - const webpPath = await this.generateThumbnail(asset, 'webp'); - await this.assetRepository.update({ id: asset.id, webpPath }); + const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP); + await this.assetRepository.update({ id: asset.id, thumbnailPath }); return JobStatus.SUCCESS; } - async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbhash({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); - if (!asset?.resizePath) { + if (!asset?.previewPath) { return JobStatus.FAILED; } - const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath); + const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath); await this.assetRepository.update({ id: asset.id, thumbhash }); return JobStatus.SUCCESS; diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index d5cae818ece38..7bea8c37702e8 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -53,9 +53,9 @@ export class MicroservicesService { [JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data), [JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data), [JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data), - [JobName.GENERATE_JPEG_THUMBNAIL]: (data) => this.mediaService.handleGenerateJpegThumbnail(data), - [JobName.GENERATE_WEBP_THUMBNAIL]: (data) => this.mediaService.handleGenerateWebpThumbnail(data), - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbhashThumbnail(data), + [JobName.GENERATE_PREVIEW]: (data) => this.mediaService.handleGeneratePreview(data), + [JobName.GENERATE_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbnail(data), + [JobName.GENERATE_THUMBHASH]: (data) => this.mediaService.handleGenerateThumbhash(data), [JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data), [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data), [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data), diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 10e42e1b6632f..501154c1db618 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -645,7 +645,7 @@ describe(PersonService.name, () => { expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', { - imagePath: assetStub.image.resizePath, + imagePath: assetStub.image.previewPath, }, { enabled: true, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 504716a55e702..d2bc81b0eae9d 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -23,6 +23,7 @@ import { } from 'src/dtos/person.dto'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { ImageFormat } from 'src/entities/system-config.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -315,17 +316,17 @@ export class PersonService { }, }; const [asset] = await this.assetRepository.getByIds([id], relations); - if (!asset || !asset.resizePath || asset.faces?.length > 0) { + if (!asset || !asset.previewPath || asset.faces?.length > 0) { return JobStatus.FAILED; } const faces = await this.machineLearningRepository.detectFaces( machineLearning.url, - { imagePath: asset.resizePath }, + { imagePath: asset.previewPath }, machineLearning.facialRecognition, ); - this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); + this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`); this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` }))); if (faces.length > 0) { @@ -470,7 +471,7 @@ export class PersonService { } async handleGeneratePersonThumbnail(data: IEntityJob): Promise { - const { machineLearning, thumbnail } = await this.configCore.getConfig(); + const { machineLearning, image } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { return JobStatus.SKIPPED; } @@ -496,7 +497,7 @@ export class PersonService { } = face; const [asset] = await this.assetRepository.getByIds([assetId]); - if (!asset?.resizePath) { + if (!asset?.previewPath) { return JobStatus.FAILED; } this.logger.verbose(`Cropping face for person: ${person.id}`); @@ -527,12 +528,12 @@ export class PersonService { height: newHalfSize * 2, }; - const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions); + const croppedOutput = await this.mediaRepository.crop(asset.previewPath, cropOptions); const thumbnailOptions = { - format: 'jpeg', + format: ImageFormat.JPEG, size: FACE_THUMBNAIL_SIZE, - colorspace: thumbnail.colorspace, - quality: thumbnail.quality, + colorspace: image.colorspace, + quality: image.quality, } as const; await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 03fa154a38cb1..9422dac86bd2b 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -76,6 +76,9 @@ export class SearchService { checksum = Buffer.from(dto.checksum, encoding); } + dto.previewPath ??= dto.resizePath; + dto.thumbnailPath ??= dto.webpPath; + const page = dto.page ?? 1; const size = dto.size || 250; const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 968aeac5f86e1..2e1dfbafc072b 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -18,7 +18,7 @@ import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.r const asset = { id: 'asset-1', - resizePath: 'path/to/resize.ext', + previewPath: 'path/to/resize.ext', } as AssetEntity; describe(SmartInfoService.name, () => { @@ -94,7 +94,7 @@ describe(SmartInfoService.name, () => { }); it('should skip assets without a resize path', async () => { - const asset = { resizePath: '' } as AssetEntity; + const asset = { previewPath: '' } as AssetEntity; assetMock.getByIds.mockResolvedValue([asset]); await sut.handleEncodeClip({ id: asset.id }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 183c45b80b630..f9d36c238cda6 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -83,13 +83,13 @@ export class SmartInfoService { return JobStatus.FAILED; } - if (!asset.resizePath) { + if (!asset.previewPath) { return JobStatus.FAILED; } const clipEmbedding = await this.machineLearning.encodeImage( machineLearning.url, - { imagePath: asset.resizePath }, + { imagePath: asset.previewPath }, machineLearning.clip, ); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index b2079f60677d1..4bb5dd0a1b410 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -4,6 +4,7 @@ import { AudioCodec, CQMode, Colorspace, + ImageFormat, LogLevel, SystemConfig, SystemConfigEntity, @@ -119,9 +120,11 @@ const updatedConfig = Object.freeze({ hashVerificationEnabled: true, template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, - thumbnail: { - webpSize: 250, - jpegSize: 1440, + image: { + thumbnailFormat: ImageFormat.WEBP, + thumbnailSize: 250, + previewFormat: ImageFormat.JPEG, + previewSize: 1440, quality: 80, colorspace: Colorspace.P3, }, diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 7ec6777561939..be0eb8fa66c4f 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -58,7 +58,7 @@ export function searchAssetBuilder( builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); } - const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'resizePath', 'webpPath']); + const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']); builder.andWhere(_.omitBy(path, _.isUndefined)); if (options.originalFileName) { diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index fc94a363a10f8..0b2ff82a3d0c4 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -26,10 +26,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_123.jpg', - resizePath: null, + previewPath: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -62,10 +62,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -102,10 +102,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -139,10 +139,10 @@ export const assetStub = { ownerId: 'admin-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/admin-id/thumbs/path.jpg', + previewPath: '/uploads/admin-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/admin-id/webp/path.ext', + thumbnailPath: '/uploads/admin-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -184,10 +184,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -224,10 +224,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -264,10 +264,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -304,10 +304,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -344,10 +344,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -385,10 +385,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -456,10 +456,10 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', sidecarPath: null, type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-22T05:06:29.716Z'), @@ -499,11 +499,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -535,11 +535,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -572,11 +572,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -610,10 +610,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -648,10 +648,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -687,10 +687,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 34e3da5156d17..ccd76c328ef7e 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -199,7 +199,7 @@ export const sharedLinkStub = { deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', - resizePath: '', + previewPath: '', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, @@ -219,7 +219,7 @@ export const sharedLinkStub = { objects: ['a', 'b', 'c'], asset: null as any, }, - webpPath: '', + thumbnailPath: '', thumbhash: null, encodedVideoPath: '', duration: null, diff --git a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte similarity index 75% rename from web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte rename to web/src/lib/components/admin-page/settings/image/image-settings.svelte index 8e2936b556c51..dcf59936d806d 100644 --- a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -25,10 +25,10 @@
(config.thumbnail.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} - isEdited={config.thumbnail.colorspace !== savedConfig.thumbnail.colorspace} + checked={config.image.colorspace === Colorspace.P3} + on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} + isEdited={config.image.colorspace !== savedConfig.image.colorspace} />
dispatch('reset', { ...detail, configKeys: ['thumbnail'] })} - on:save={() => dispatch('save', { thumbnail: config.thumbnail })} + on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['image'] })} + on:save={() => dispatch('save', { image: config.image })} showResetToDefault={!isEqual(savedConfig, defaultConfig)} {disabled} /> diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index d63f9544a36b3..cb80a9e0d6f14 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -13,7 +13,7 @@ import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; import ThemeSettings from '$lib/components/admin-page/settings/theme/theme-settings.svelte'; - import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; + import ImageSettings from '$lib/components/admin-page/settings/image/image-settings.svelte'; import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte'; import UserSettings from '$lib/components/admin-page/settings/user-settings/user-settings.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; @@ -43,7 +43,7 @@ | typeof ServerSettings | typeof StorageTemplateSettings | typeof ThemeSettings - | typeof ThumbnailSettings + | typeof ImageSettings | typeof TrashSettings | typeof NewVersionCheckSettings | typeof FFmpegSettings @@ -64,6 +64,12 @@ subtitle: string; key: string; }> = [ + { + item: ImageSettings, + title: 'Image Settings', + subtitle: 'Manage the quality and resolution of generated images', + key: 'image', + }, { item: JobSettings, title: 'Job Settings', @@ -124,12 +130,6 @@ subtitle: 'Manage customization of the Immich web interface', key: 'theme', }, - { - item: ThumbnailSettings, - title: 'Thumbnail Settings', - subtitle: 'Manage the resolution of thumbnail sizes', - key: 'thumbnail', - }, { item: TrashSettings, title: 'Trash Settings',