mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
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 <alex.tran1502@gmail.com>
This commit is contained in:
parent
e520c0d1f5
commit
8edc2fb46f
66 changed files with 533 additions and 383 deletions
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
9
mobile/openapi/.openapi-generator/FILES
generated
9
mobile/openapi/.openapi-generator/FILES
generated
|
@ -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
|
||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/ImageFormat.md
generated
Normal file
BIN
mobile/openapi/doc/ImageFormat.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
BIN
mobile/openapi/doc/SystemConfigDto.md
generated
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/image_format.dart
generated
Normal file
BIN
mobile/openapi/lib/model/image_format.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/path_type.dart
generated
BIN
mobile/openapi/lib/model/path_type.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
BIN
mobile/openapi/lib/model/system_config_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/system_config_image_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/system_config_image_dto.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/image_format_test.dart
generated
Normal file
BIN
mobile/openapi/test/image_format_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
BIN
mobile/openapi/test/system_config_dto_test.dart
generated
Binary file not shown.
Binary file not shown.
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
AudioCodec,
|
||||
CQMode,
|
||||
Colorspace,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
SystemConfig,
|
||||
SystemConfigEntity,
|
||||
|
@ -112,9 +113,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -117,8 +117,8 @@ export interface SearchPathOptions {
|
|||
encodedVideoPath?: string;
|
||||
originalFileName?: string;
|
||||
originalPath?: string;
|
||||
resizePath?: string;
|
||||
webpPath?: string;
|
||||
previewPath?: string;
|
||||
thumbnailPath?: string;
|
||||
}
|
||||
|
||||
export interface SearchExifOptions {
|
||||
|
|
51
server/src/migrations/1711257900274-RenameWebpJpegPaths.ts
Normal file
51
server/src/migrations/1711257900274-RenameWebpJpegPaths.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RenameWebpJpegPaths1711257900274 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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'`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -66,7 +66,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
|
|||
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> {
|
||||
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<CuratedLocationsResponseDto[]> {
|
||||
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
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -35,9 +35,9 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
|||
|
||||
// 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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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<JobStatus> {
|
||||
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<JobStatus> {
|
||||
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
||||
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<JobStatus> {
|
||||
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
||||
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<JobStatus> {
|
||||
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
|
||||
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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<JobStatus> {
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
AudioCodec,
|
||||
CQMode,
|
||||
Colorspace,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
SystemConfig,
|
||||
SystemConfigEntity,
|
||||
|
@ -119,9 +120,11 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
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,
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
68
server/test/fixtures/asset.stub.ts
vendored
68
server/test/fixtures/asset.stub.ts
vendored
|
@ -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'),
|
||||
|
|
4
server/test/fixtures/shared-link.stub.ts
vendored
4
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -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,
|
||||
|
|
|
@ -25,10 +25,10 @@
|
|||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="SMALL THUMBNAIL RESOLUTION"
|
||||
label="THUMBNAIL RESOLUTION"
|
||||
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
number
|
||||
bind:value={config.thumbnail.webpSize}
|
||||
bind:value={config.image.thumbnailSize}
|
||||
options={[
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
|
@ -37,15 +37,15 @@
|
|||
{ value: 200, text: '200p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={config.thumbnail.webpSize !== savedConfig.thumbnail.webpSize}
|
||||
isEdited={config.image.thumbnailSize !== savedConfig.image.thumbnailSize}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="LARGE THUMBNAIL RESOLUTION"
|
||||
label="PREVIEW RESOLUTION"
|
||||
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
number
|
||||
bind:value={config.thumbnail.jpegSize}
|
||||
bind:value={config.image.previewSize}
|
||||
options={[
|
||||
{ value: 2160, text: '4K' },
|
||||
{ value: 1440, text: '1440p' },
|
||||
|
@ -53,31 +53,31 @@
|
|||
{ value: 720, text: '720p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={config.thumbnail.jpegSize !== savedConfig.thumbnail.jpegSize}
|
||||
isEdited={config.image.previewSize !== savedConfig.image.previewSize}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="QUALITY"
|
||||
desc="Thumbnail quality from 1-100. Higher is better for quality but produces larger files."
|
||||
bind:value={config.thumbnail.quality}
|
||||
isEdited={config.thumbnail.quality !== savedConfig.thumbnail.quality}
|
||||
desc="Image quality from 1-100. Higher is better for quality but produces larger files."
|
||||
bind:value={config.image.quality}
|
||||
isEdited={config.image.quality !== savedConfig.image.quality}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="PREFER WIDE GAMUT"
|
||||
subtitle="Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts."
|
||||
checked={config.thumbnail.colorspace === Colorspace.P3}
|
||||
on:toggle={(e) => (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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => 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}
|
||||
/>
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue