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 fe369f899e..e8252f25d6 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 9a1d1acb1b..a890d674bc 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 9ec77670fb..795943e299 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 bfdac06c48..a46f2383e5 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 0778485c37..297a4cdba6 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 0000000000..312e501c17 --- /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 d1d098fb0e..5dc50c00fa 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 ad1afbe9fc..fc3d3dd0b7 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 491bf9f120..1b9bbe726d 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 2abc20fde8..1600dfb331 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 e16ccc73e5..10f1c0c10e 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 312153788c..4a145d0c44 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 d186845d94..9d2d86cba5 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 0000000000..570b6ca6e6 --- /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 86a2856e66..3f770ed092 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 ea722076d9..11cdf41ea1 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 0b5f64fc27..f075d37c86 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 0000000000..1c830861af --- /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 f1d55d6220..0000000000 --- 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 41d0ac8f5c..0c1729e95f 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 0000000000..2bb1512a68 --- /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 f817b7da74..62979da9c0 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 b41d07e5f9..e88ee17c4b 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 3cc66f4677..aef907bbe6 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 a2aae92c41..16a9ad63af 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 b9b4978a9b..e63ccb4d64 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 ee9f12e518..035f90c911 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 01bfacc9bd..3a1ea47bbe 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 cf16a99a29..bdda36d15e 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 799baddee3..d96ce0d98a 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 740f1672ee..9f80e8d6a3 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 9166361153..c977560bea 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 de20cb9737..f3dad6b280 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 98b882a36e..e07b6d4a22 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 6f07fc7525..eddaefcf38 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 0e34227d33..5e51e94a52 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 1287202ade..771b23e9c9 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 0000000000..ab6f2a4e9f --- /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 fab0f5376f..6f0e8cd5e3 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 b6a513ff94..1cde746d8b 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 e985a1a6d7..3e83d72384 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 27531cfc9e..78581b8ba1 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 6f53d820c1..229e700fd5 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 907cddb893..e6389c2e56 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 b55996ed05..a7c99f93cb 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 898fb5a99f..735ac8322a 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 a24ddbd69d..97aa99d91d 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 683e2f5ae7..b90c194aa0 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 556883afd4..135377e0bd 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 ff5e0d9c79..d40167429f 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 aa5739878b..ac0e502ae8 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 c03b7c7bc2..3f9cd8a221 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 4397730ab6..3a650430ef 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 26aa2dce9f..c56fd26e6a 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 d5cae818ec..7bea8c3770 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 10e42e1b66..501154c1db 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 504716a55e..d2bc81b0ea 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 03fa154a38..9422dac86b 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 968aeac5f8..2e1dfbafc0 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 183c45b80b..f9d36c238c 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 b2079f6067..4bb5dd0a1b 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 7ec6777561..be0eb8fa66 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 fc94a363a1..0b2ff82a3d 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 34e3da5156..ccd76c328e 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 8e2936b556..dcf59936d8 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 d63f9544a3..cb80a9e0d6 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',