1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +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:
Mert 2024-04-02 00:56:56 -04:00 committed by GitHub
parent e520c0d1f5
commit 8edc2fb46f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 533 additions and 383 deletions

View file

@ -45,7 +45,7 @@ SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "ex
``` ```
```sql title="Without thumbnails" ```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" ```sql title="By type"

View file

@ -114,9 +114,11 @@ The default configuration looks like this:
"hashVerificationEnabled": true, "hashVerificationEnabled": true,
"template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
}, },
"thumbnail": { "image": {
"webpSize": 250, "thumbnailFormat": "webp",
"jpegSize": 1440, "thumbnailSize": 250,
"previewFormat": "jpeg",
"previewSize": 1440,
"quality": 80, "quality": 80,
"colorspace": "p3" "colorspace": "p3"
}, },

View file

@ -72,6 +72,7 @@ doc/FileChecksumResponseDto.md
doc/FileReportDto.md doc/FileReportDto.md
doc/FileReportFixDto.md doc/FileReportFixDto.md
doc/FileReportItemDto.md doc/FileReportItemDto.md
doc/ImageFormat.md
doc/JobApi.md doc/JobApi.md
doc/JobCommand.md doc/JobCommand.md
doc/JobCommandDto.md doc/JobCommandDto.md
@ -145,6 +146,7 @@ doc/SmartSearchDto.md
doc/SystemConfigApi.md doc/SystemConfigApi.md
doc/SystemConfigDto.md doc/SystemConfigDto.md
doc/SystemConfigFFmpegDto.md doc/SystemConfigFFmpegDto.md
doc/SystemConfigImageDto.md
doc/SystemConfigJobDto.md doc/SystemConfigJobDto.md
doc/SystemConfigLibraryDto.md doc/SystemConfigLibraryDto.md
doc/SystemConfigLibraryScanDto.md doc/SystemConfigLibraryScanDto.md
@ -160,7 +162,6 @@ doc/SystemConfigServerDto.md
doc/SystemConfigStorageTemplateDto.md doc/SystemConfigStorageTemplateDto.md
doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThemeDto.md doc/SystemConfigThemeDto.md
doc/SystemConfigThumbnailDto.md
doc/SystemConfigTrashDto.md doc/SystemConfigTrashDto.md
doc/SystemConfigUserDto.md doc/SystemConfigUserDto.md
doc/TagApi.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_dto.dart
lib/model/file_report_fix_dto.dart lib/model/file_report_fix_dto.dart
lib/model/file_report_item_dto.dart lib/model/file_report_item_dto.dart
lib/model/image_format.dart
lib/model/job_command.dart lib/model/job_command.dart
lib/model/job_command_dto.dart lib/model/job_command_dto.dart
lib/model/job_counts_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/smart_search_dto.dart
lib/model/system_config_dto.dart lib/model/system_config_dto.dart
lib/model/system_config_f_fmpeg_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_job_dto.dart
lib/model/system_config_library_dto.dart lib/model/system_config_library_dto.dart
lib/model/system_config_library_scan_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_storage_template_dto.dart
lib/model/system_config_template_storage_option_dto.dart lib/model/system_config_template_storage_option_dto.dart
lib/model/system_config_theme_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_trash_dto.dart
lib/model/system_config_user_dto.dart lib/model/system_config_user_dto.dart
lib/model/tag_response_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_dto_test.dart
test/file_report_fix_dto_test.dart test/file_report_fix_dto_test.dart
test/file_report_item_dto_test.dart test/file_report_item_dto_test.dart
test/image_format_test.dart
test/job_api_test.dart test/job_api_test.dart
test/job_command_dto_test.dart test/job_command_dto_test.dart
test/job_command_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_api_test.dart
test/system_config_dto_test.dart test/system_config_dto_test.dart
test/system_config_f_fmpeg_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_job_dto_test.dart
test/system_config_library_dto_test.dart test/system_config_library_dto_test.dart
test/system_config_library_scan_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_storage_template_dto_test.dart
test/system_config_template_storage_option_dto_test.dart test/system_config_template_storage_option_dto_test.dart
test/system_config_theme_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_trash_dto_test.dart
test/system_config_user_dto_test.dart test/system_config_user_dto_test.dart
test/tag_api_test.dart test/tag_api_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/ImageFormat.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/lib/model/image_format.dart generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/test/image_format_test.dart generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2101,10 +2101,19 @@
} }
} }
}, },
{
"name": "previewPath",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{ {
"name": "resizePath", "name": "resizePath",
"required": false, "required": false,
"in": "query", "in": "query",
"deprecated": true,
"schema": { "schema": {
"type": "string" "type": "string"
} }
@ -2143,6 +2152,14 @@
"type": "string" "type": "string"
} }
}, },
{
"name": "thumbnailPath",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{ {
"name": "trashedAfter", "name": "trashedAfter",
"required": false, "required": false,
@ -2191,6 +2208,7 @@
"name": "webpPath", "name": "webpPath",
"required": false, "required": false,
"in": "query", "in": "query",
"deprecated": true,
"schema": { "schema": {
"type": "string" "type": "string"
} }
@ -8114,6 +8132,13 @@
], ],
"type": "object" "type": "object"
}, },
"ImageFormat": {
"enum": [
"jpeg",
"webp"
],
"type": "string"
},
"JobCommand": { "JobCommand": {
"enum": [ "enum": [
"start", "start",
@ -8555,7 +8580,11 @@
}, },
"type": "array" "type": "array"
}, },
"previewPath": {
"type": "string"
},
"resizePath": { "resizePath": {
"deprecated": true,
"type": "string" "type": "string"
}, },
"size": { "size": {
@ -8572,6 +8601,9 @@
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
}, },
"thumbnailPath": {
"type": "string"
},
"trashedAfter": { "trashedAfter": {
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
@ -8592,6 +8624,7 @@
"type": "string" "type": "string"
}, },
"webpPath": { "webpPath": {
"deprecated": true,
"type": "string" "type": "string"
}, },
"withArchived": { "withArchived": {
@ -8746,8 +8779,8 @@
"PathType": { "PathType": {
"enum": [ "enum": [
"original", "original",
"jpeg_thumbnail", "preview",
"webp_thumbnail", "thumbnail",
"encoded_video", "encoded_video",
"sidecar", "sidecar",
"face", "face",
@ -9743,6 +9776,9 @@
"ffmpeg": { "ffmpeg": {
"$ref": "#/components/schemas/SystemConfigFFmpegDto" "$ref": "#/components/schemas/SystemConfigFFmpegDto"
}, },
"image": {
"$ref": "#/components/schemas/SystemConfigImageDto"
},
"job": { "job": {
"$ref": "#/components/schemas/SystemConfigJobDto" "$ref": "#/components/schemas/SystemConfigJobDto"
}, },
@ -9779,9 +9815,6 @@
"theme": { "theme": {
"$ref": "#/components/schemas/SystemConfigThemeDto" "$ref": "#/components/schemas/SystemConfigThemeDto"
}, },
"thumbnail": {
"$ref": "#/components/schemas/SystemConfigThumbnailDto"
},
"trash": { "trash": {
"$ref": "#/components/schemas/SystemConfigTrashDto" "$ref": "#/components/schemas/SystemConfigTrashDto"
}, },
@ -9791,6 +9824,7 @@
}, },
"required": [ "required": [
"ffmpeg", "ffmpeg",
"image",
"job", "job",
"library", "library",
"logging", "logging",
@ -9803,7 +9837,6 @@
"server", "server",
"storageTemplate", "storageTemplate",
"theme", "theme",
"thumbnail",
"trash", "trash",
"user" "user"
], ],
@ -9902,6 +9935,37 @@
], ],
"type": "object" "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": { "SystemConfigJobDto": {
"properties": { "properties": {
"backgroundTask": { "backgroundTask": {
@ -10251,29 +10315,6 @@
], ],
"type": "object" "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": { "SystemConfigTrashDto": {
"properties": { "properties": {
"days": { "days": {

View file

@ -640,11 +640,13 @@ export type MetadataSearchDto = {
originalPath?: string; originalPath?: string;
page?: number; page?: number;
personIds?: string[]; personIds?: string[];
previewPath?: string;
resizePath?: string; resizePath?: string;
size?: number; size?: number;
state?: string; state?: string;
takenAfter?: string; takenAfter?: string;
takenBefore?: string; takenBefore?: string;
thumbnailPath?: string;
trashedAfter?: string; trashedAfter?: string;
trashedBefore?: string; trashedBefore?: string;
"type"?: AssetTypeEnum; "type"?: AssetTypeEnum;
@ -827,6 +829,14 @@ export type SystemConfigFFmpegDto = {
transcode: TranscodePolicy; transcode: TranscodePolicy;
twoPass: boolean; twoPass: boolean;
}; };
export type SystemConfigImageDto = {
colorspace: Colorspace;
previewFormat: ImageFormat;
previewSize: number;
quality: number;
thumbnailFormat: ImageFormat;
thumbnailSize: number;
};
export type JobSettingsDto = { export type JobSettingsDto = {
concurrency: number; concurrency: number;
}; };
@ -919,12 +929,6 @@ export type SystemConfigStorageTemplateDto = {
export type SystemConfigThemeDto = { export type SystemConfigThemeDto = {
customCss: string; customCss: string;
}; };
export type SystemConfigThumbnailDto = {
colorspace: Colorspace;
jpegSize: number;
quality: number;
webpSize: number;
};
export type SystemConfigTrashDto = { export type SystemConfigTrashDto = {
days: number; days: number;
enabled: boolean; enabled: boolean;
@ -934,6 +938,7 @@ export type SystemConfigUserDto = {
}; };
export type SystemConfigDto = { export type SystemConfigDto = {
ffmpeg: SystemConfigFFmpegDto; ffmpeg: SystemConfigFFmpegDto;
image: SystemConfigImageDto;
job: SystemConfigJobDto; job: SystemConfigJobDto;
library: SystemConfigLibraryDto; library: SystemConfigLibraryDto;
logging: SystemConfigLoggingDto; logging: SystemConfigLoggingDto;
@ -946,7 +951,6 @@ export type SystemConfigDto = {
server: SystemConfigServerDto; server: SystemConfigServerDto;
storageTemplate: SystemConfigStorageTemplateDto; storageTemplate: SystemConfigStorageTemplateDto;
theme: SystemConfigThemeDto; theme: SystemConfigThemeDto;
thumbnail: SystemConfigThumbnailDto;
trash: SystemConfigTrashDto; trash: SystemConfigTrashDto;
user: SystemConfigUserDto; user: SystemConfigUserDto;
}; };
@ -1497,7 +1501,7 @@ export function updateAsset({ id, updateAssetDto }: {
body: 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; checksum?: string;
city?: string; city?: string;
country?: string; country?: string;
@ -1525,11 +1529,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef
originalPath?: string; originalPath?: string;
page?: number; page?: number;
personIds?: string[]; personIds?: string[];
previewPath?: string;
resizePath?: string; resizePath?: string;
size?: number; size?: number;
state?: string; state?: string;
takenAfter?: string; takenAfter?: string;
takenBefore?: string; takenBefore?: string;
thumbnailPath?: string;
trashedAfter?: string; trashedAfter?: string;
trashedBefore?: string; trashedBefore?: string;
$type?: AssetTypeEnum; $type?: AssetTypeEnum;
@ -1573,11 +1579,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef
originalPath, originalPath,
page, page,
personIds, personIds,
previewPath,
resizePath, resizePath,
size, size,
state, state,
takenAfter, takenAfter,
takenBefore, takenBefore,
thumbnailPath,
trashedAfter, trashedAfter,
trashedBefore, trashedBefore,
"type": $type, "type": $type,
@ -2802,8 +2810,8 @@ export enum PathEntityType {
} }
export enum PathType { export enum PathType {
Original = "original", Original = "original",
JpegThumbnail = "jpeg_thumbnail", Preview = "preview",
WebpThumbnail = "webp_thumbnail", Thumbnail = "thumbnail",
EncodedVideo = "encoded_video", EncodedVideo = "encoded_video",
Sidecar = "sidecar", Sidecar = "sidecar",
Face = "face", Face = "face",
@ -2885,6 +2893,14 @@ export enum TranscodePolicy {
Required = "required", Required = "required",
Disabled = "disabled" Disabled = "disabled"
} }
export enum Colorspace {
Srgb = "srgb",
P3 = "p3"
}
export enum ImageFormat {
Jpeg = "jpeg",
Webp = "webp"
}
export enum LogLevel { export enum LogLevel {
Verbose = "verbose", Verbose = "verbose",
Debug = "debug", Debug = "debug",
@ -2901,10 +2917,6 @@ export enum ModelType {
FacialRecognition = "facial-recognition", FacialRecognition = "facial-recognition",
Clip = "clip" Clip = "clip"
} }
export enum Colorspace {
Srgb = "srgb",
P3 = "p3"
}
export enum MapTheme { export enum MapTheme {
Light = "light", Light = "light",
Dark = "dark" Dark = "dark"

View file

@ -4,6 +4,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { ImageFormat } from 'src/entities/system-config.entity';
import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IMoveRepository } from 'src/interfaces/move.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; let instance: StorageCore | null;
@ -94,12 +96,8 @@ export class StorageCore {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
} }
static getLargeThumbnailPath(asset: AssetEntity) { static getImagePath(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.jpeg`); return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
}
static getSmallThumbnailPath(asset: AssetEntity) {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.webp`);
} }
static getEncodedVideoPath(asset: AssetEntity) { static getEncodedVideoPath(asset: AssetEntity) {
@ -128,34 +126,23 @@ export class StorageCore {
return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR); return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR);
} }
async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) {
const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; const { id: entityId, previewPath, thumbnailPath } = asset;
switch (pathType) { return this.moveFile({
case AssetPathType.JPEG_THUMBNAIL: { entityId,
return this.moveFile({ pathType,
entityId, oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath,
pathType, newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format),
oldPath: resizePath, });
newPath: StorageCore.getLargeThumbnailPath(asset), }
});
} async moveAssetVideo(asset: AssetEntity) {
case AssetPathType.WEBP_THUMBNAIL: { return this.moveFile({
return this.moveFile({ entityId: asset.id,
entityId, pathType: AssetPathType.ENCODED_VIDEO,
pathType, oldPath: asset.encodedVideoPath,
oldPath: webpPath, newPath: StorageCore.getEncodedVideoPath(asset),
newPath: StorageCore.getSmallThumbnailPath(asset), });
});
}
case AssetPathType.ENCODED_VIDEO: {
return this.moveFile({
entityId,
pathType,
oldPath: encodedVideoPath,
newPath: StorageCore.getEncodedVideoPath(asset),
});
}
}
} }
async movePersonFile(person: PersonEntity, pathType: PersonPathType) { async movePersonFile(person: PersonEntity, pathType: PersonPathType) {
@ -294,11 +281,11 @@ export class StorageCore {
case AssetPathType.ORIGINAL: { case AssetPathType.ORIGINAL: {
return this.assetRepository.update({ id, originalPath: newPath }); return this.assetRepository.update({ id, originalPath: newPath });
} }
case AssetPathType.JPEG_THUMBNAIL: { case AssetPathType.PREVIEW: {
return this.assetRepository.update({ id, resizePath: newPath }); return this.assetRepository.update({ id, previewPath: newPath });
} }
case AssetPathType.WEBP_THUMBNAIL: { case AssetPathType.THUMBNAIL: {
return this.assetRepository.update({ id, webpPath: newPath }); return this.assetRepository.update({ id, thumbnailPath: newPath });
} }
case AssetPathType.ENCODED_VIDEO: { case AssetPathType.ENCODED_VIDEO: {
return this.assetRepository.update({ id, encodedVideoPath: newPath }); return this.assetRepository.update({ id, encodedVideoPath: newPath });

View file

@ -10,6 +10,7 @@ import {
AudioCodec, AudioCodec,
CQMode, CQMode,
Colorspace, Colorspace,
ImageFormat,
LogLevel, LogLevel,
SystemConfig, SystemConfig,
SystemConfigEntity, SystemConfigEntity,
@ -112,9 +113,11 @@ export const defaults = Object.freeze<SystemConfig>({
hashVerificationEnabled: true, hashVerificationEnabled: true,
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
}, },
thumbnail: { image: {
webpSize: 250, thumbnailFormat: ImageFormat.WEBP,
jpegSize: 1440, thumbnailSize: 250,
previewFormat: ImageFormat.JPEG,
previewSize: 1440,
quality: 80, quality: 80,
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
}, },

View file

@ -82,7 +82,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
type: entity.type, type: entity.type,
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime, localDateTime: entity.localDateTime,
resized: !!entity.resizePath, resized: !!entity.previewPath,
duration: entity.duration ?? '0:00:00.00000', duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false, hasMetadata: false,
@ -100,7 +100,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
type: entity.type, type: entity.type,
originalPath: entity.originalPath, originalPath: entity.originalPath,
originalFileName: entity.originalFileName, originalFileName: entity.originalFileName,
resized: !!entity.resizePath, resized: !!entity.previewPath,
thumbhash: entity.thumbhash?.toString('base64') ?? null, thumbhash: entity.thumbhash?.toString('base64') ?? null,
fileCreatedAt: entity.fileCreatedAt, fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt, fileModifiedAt: entity.fileModifiedAt,

View file

@ -163,13 +163,25 @@ export class MetadataSearchDto extends BaseSearchDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Optional() @Optional()
@ApiProperty({ deprecated: true })
resizePath?: string; resizePath?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Optional() @Optional()
@ApiProperty({ deprecated: true })
webpPath?: string; webpPath?: string;
@IsString()
@IsNotEmpty()
@Optional()
previewPath?: string;
@IsString()
@IsNotEmpty()
@Optional()
thumbnailPath?: string;
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@Optional() @Optional()

View file

@ -22,6 +22,7 @@ import {
AudioCodec, AudioCodec,
CQMode, CQMode,
Colorspace, Colorspace,
ImageFormat,
LogLevel, LogLevel,
SystemConfig, SystemConfig,
ToneMapping, ToneMapping,
@ -385,18 +386,26 @@ export class SystemConfigThemeDto {
customCss!: string; customCss!: string;
} }
class SystemConfigThumbnailDto { class SystemConfigImageDto {
@IsInt() @IsEnum(ImageFormat)
@Min(1) @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
@Type(() => Number) thumbnailFormat!: ImageFormat;
@ApiProperty({ type: 'integer' })
webpSize!: number;
@IsInt() @IsInt()
@Min(1) @Min(1)
@Type(() => Number) @Type(() => Number)
@ApiProperty({ type: 'integer' }) @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() @IsInt()
@Min(1) @Min(1)
@ -480,10 +489,10 @@ export class SystemConfigDto implements SystemConfig {
@IsObject() @IsObject()
job!: SystemConfigJobDto; job!: SystemConfigJobDto;
@Type(() => SystemConfigThumbnailDto) @Type(() => SystemConfigImageDto)
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
thumbnail!: SystemConfigThumbnailDto; image!: SystemConfigImageDto;
@Type(() => SystemConfigTrashDto) @Type(() => SystemConfigTrashDto)
@ValidateNested() @ValidateNested()

View file

@ -67,10 +67,10 @@ export class AssetEntity {
originalPath!: string; originalPath!: string;
@Column({ type: 'varchar', nullable: true }) @Column({ type: 'varchar', nullable: true })
resizePath!: string | null; previewPath!: string | null;
@Column({ type: 'varchar', nullable: true, default: '' }) @Column({ type: 'varchar', nullable: true, default: '' })
webpPath!: string | null; thumbnailPath!: string | null;
@Column({ type: 'bytea', nullable: true }) @Column({ type: 'bytea', nullable: true })
thumbhash!: Buffer | null; thumbhash!: Buffer | null;

View file

@ -24,8 +24,8 @@ export class MoveEntity {
export enum AssetPathType { export enum AssetPathType {
ORIGINAL = 'original', ORIGINAL = 'original',
JPEG_THUMBNAIL = 'jpeg_thumbnail', PREVIEW = 'preview',
WEBP_THUMBNAIL = 'webp_thumbnail', THUMBNAIL = 'thumbnail',
ENCODED_VIDEO = 'encoded_video', ENCODED_VIDEO = 'encoded_video',
SIDECAR = 'sidecar', SIDECAR = 'sidecar',
} }

View file

@ -165,6 +165,11 @@ export enum Colorspace {
P3 = 'p3', P3 = 'p3',
} }
export enum ImageFormat {
JPEG = 'jpeg',
WEBP = 'webp',
}
export enum LogLevel { export enum LogLevel {
VERBOSE = 'verbose', VERBOSE = 'verbose',
DEBUG = 'debug', DEBUG = 'debug',
@ -249,9 +254,11 @@ export interface SystemConfig {
hashVerificationEnabled: boolean; hashVerificationEnabled: boolean;
template: string; template: string;
}; };
thumbnail: { image: {
webpSize: number; thumbnailFormat: ImageFormat;
jpegSize: number; thumbnailSize: number;
previewFormat: ImageFormat;
previewSize: number;
quality: number; quality: number;
colorspace: Colorspace; colorspace: Colorspace;
}; };

View file

@ -33,9 +33,9 @@ export enum JobName {
// thumbnails // thumbnails
QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails',
GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', GENERATE_PREVIEW = 'generate-preview',
GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', GENERATE_THUMBNAIL = 'generate-thumbnail',
GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail', GENERATE_THUMBHASH = 'generate-thumbhash',
GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
// metadata // metadata
@ -160,9 +160,9 @@ export type JobItem =
// Thumbnails // Thumbnails
| { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob } | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob }
| { name: JobName.GENERATE_JPEG_THUMBNAIL; data: IEntityJob } | { name: JobName.GENERATE_PREVIEW; data: IEntityJob }
| { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob } | { name: JobName.GENERATE_THUMBNAIL; data: IEntityJob }
| { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob } | { name: JobName.GENERATE_THUMBHASH; data: IEntityJob }
// User // User
| { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob }

View file

@ -1,11 +1,11 @@
import { Writable } from 'node:stream'; 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 const IMediaRepository = 'IMediaRepository';
export interface ResizeOptions { export interface ResizeOptions {
size: number; size: number;
format: 'webp' | 'jpeg'; format: ImageFormat;
colorspace: string; colorspace: string;
quality: number; quality: number;
} }

View file

@ -117,8 +117,8 @@ export interface SearchPathOptions {
encodedVideoPath?: string; encodedVideoPath?: string;
originalFileName?: string; originalFileName?: string;
originalPath?: string; originalPath?: string;
resizePath?: string; previewPath?: string;
webpPath?: string; thumbnailPath?: string;
} }
export interface SearchExifOptions { export interface SearchExifOptions {

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

View file

@ -9,8 +9,8 @@ SELECT
"entity"."deviceId" AS "entity_deviceId", "entity"."deviceId" AS "entity_deviceId",
"entity"."type" AS "entity_type", "entity"."type" AS "entity_type",
"entity"."originalPath" AS "entity_originalPath", "entity"."originalPath" AS "entity_originalPath",
"entity"."resizePath" AS "entity_resizePath", "entity"."previewPath" AS "entity_previewPath",
"entity"."webpPath" AS "entity_webpPath", "entity"."thumbnailPath" AS "entity_thumbnailPath",
"entity"."thumbhash" AS "entity_thumbhash", "entity"."thumbhash" AS "entity_thumbhash",
"entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."encodedVideoPath" AS "entity_encodedVideoPath",
"entity"."createdAt" AS "entity_createdAt", "entity"."createdAt" AS "entity_createdAt",
@ -67,7 +67,7 @@ WHERE
"entity"."ownerId" IN ($1) "entity"."ownerId" IN ($1)
AND "entity"."isVisible" = true AND "entity"."isVisible" = true
AND "entity"."isArchived" = false AND "entity"."isArchived" = false
AND "entity"."resizePath" IS NOT NULL AND "entity"."previewPath" IS NOT NULL
AND EXTRACT( AND EXTRACT(
DAY DAY
FROM FROM
@ -92,8 +92,8 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -128,8 +128,8 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -213,8 +213,8 @@ SELECT
"bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId",
"bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type",
"bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."resizePath" AS "bd93d5747511a4dad4923546c51365bf1a803774_resizePath", "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."webpPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_webpPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash",
"bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath",
"bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt",
@ -294,8 +294,8 @@ FROM
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -391,8 +391,8 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -437,8 +437,8 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -481,8 +481,8 @@ SELECT
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -570,8 +570,8 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath", "asset"."previewPath" AS "asset_previewPath",
"asset"."webpPath" AS "asset_webpPath", "asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -629,8 +629,8 @@ SELECT
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."resizePath" AS "stackedAssets_resizePath", "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."webpPath" AS "stackedAssets_webpPath", "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",

View file

@ -152,8 +152,8 @@ FROM
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
"AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath",
"AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath",
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
@ -250,8 +250,8 @@ FROM
"AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId",
"AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."type" AS "AssetEntity_type",
"AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."originalPath" AS "AssetEntity_originalPath",
"AssetEntity"."resizePath" AS "AssetEntity_resizePath", "AssetEntity"."previewPath" AS "AssetEntity_previewPath",
"AssetEntity"."webpPath" AS "AssetEntity_webpPath", "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath",
"AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash",
"AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath",
"AssetEntity"."createdAt" AS "AssetEntity_createdAt", "AssetEntity"."createdAt" AS "AssetEntity_createdAt",
@ -380,8 +380,8 @@ SELECT
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
"AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath",
"AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath",
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",

View file

@ -14,8 +14,8 @@ FROM
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath", "asset"."previewPath" AS "asset_previewPath",
"asset"."webpPath" AS "asset_webpPath", "asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -45,8 +45,8 @@ FROM
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."resizePath" AS "stackedAssets_resizePath", "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."webpPath" AS "stackedAssets_webpPath", "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -110,8 +110,8 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath", "asset"."previewPath" AS "asset_previewPath",
"asset"."webpPath" AS "asset_webpPath", "asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",
@ -141,8 +141,8 @@ SELECT
"stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."resizePath" AS "stackedAssets_resizePath", "stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."webpPath" AS "stackedAssets_webpPath", "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt", "stackedAssets"."createdAt" AS "stackedAssets_createdAt",
@ -320,8 +320,8 @@ SELECT
"asset"."deviceId" AS "asset_deviceId", "asset"."deviceId" AS "asset_deviceId",
"asset"."type" AS "asset_type", "asset"."type" AS "asset_type",
"asset"."originalPath" AS "asset_originalPath", "asset"."originalPath" AS "asset_originalPath",
"asset"."resizePath" AS "asset_resizePath", "asset"."previewPath" AS "asset_previewPath",
"asset"."webpPath" AS "asset_webpPath", "asset"."thumbnailPath" AS "asset_thumbnailPath",
"asset"."thumbhash" AS "asset_thumbhash", "asset"."thumbhash" AS "asset_thumbhash",
"asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."encodedVideoPath" AS "asset_encodedVideoPath",
"asset"."createdAt" AS "asset_createdAt", "asset"."createdAt" AS "asset_createdAt",

View file

@ -28,8 +28,8 @@ FROM
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
"SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath",
"SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath",
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
"SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",
@ -95,8 +95,8 @@ FROM
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."resizePath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_resizePath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."webpPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_webpPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt",
@ -218,8 +218,8 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId",
"SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type",
"SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath",
"SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath",
"SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath",
"SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash",
"SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath",
"SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt",

View file

@ -66,7 +66,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> { getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> {
return this.assetRepository.query( 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 FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId" LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."ownerId" = $1 WHERE a."ownerId" = $1
@ -80,7 +80,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 {
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]> { getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]> {
return this.assetRepository.query( 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 FROM assets a
LEFT JOIN exif e ON a.id = e."assetId" LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."ownerId" = $1 WHERE a."ownerId" = $1

View file

@ -83,7 +83,7 @@ export class AssetRepository implements IAssetRepository {
`entity.ownerId IN (:...ownerIds) `entity.ownerId IN (:...ownerIds)
AND entity.isVisible = true AND entity.isVisible = true
AND entity.isArchived = false 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(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day
AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`,
{ {
@ -302,10 +302,10 @@ export class AssetRepository implements IAssetRepository {
switch (property) { switch (property) {
case WithoutProperty.THUMBNAIL: { case WithoutProperty.THUMBNAIL: {
where = [ where = [
{ resizePath: IsNull(), isVisible: true }, { previewPath: IsNull(), isVisible: true },
{ resizePath: '', isVisible: true }, { previewPath: '', isVisible: true },
{ webpPath: IsNull(), isVisible: true }, { thumbnailPath: IsNull(), isVisible: true },
{ webpPath: '', isVisible: true }, { thumbnailPath: '', isVisible: true },
{ thumbhash: IsNull(), isVisible: true }, { thumbhash: IsNull(), isVisible: true },
]; ];
break; break;
@ -339,7 +339,7 @@ export class AssetRepository implements IAssetRepository {
}; };
where = { where = {
isVisible: true, isVisible: true,
resizePath: Not(IsNull()), previewPath: Not(IsNull()),
smartSearch: { smartSearch: {
embedding: IsNull(), embedding: IsNull(),
}, },
@ -352,7 +352,7 @@ export class AssetRepository implements IAssetRepository {
smartInfo: true, smartInfo: true,
}; };
where = { where = {
resizePath: Not(IsNull()), previewPath: Not(IsNull()),
isVisible: true, isVisible: true,
smartInfo: { smartInfo: {
tags: IsNull(), tags: IsNull(),
@ -367,7 +367,7 @@ export class AssetRepository implements IAssetRepository {
jobStatus: true, jobStatus: true,
}; };
where = { where = {
resizePath: Not(IsNull()), previewPath: Not(IsNull()),
isVisible: true, isVisible: true,
faces: { faces: {
assetId: IsNull(), assetId: IsNull(),
@ -385,7 +385,7 @@ export class AssetRepository implements IAssetRepository {
faces: true, faces: true,
}; };
where = { where = {
resizePath: Not(IsNull()), previewPath: Not(IsNull()),
isVisible: true, isVisible: true,
faces: { faces: {
assetId: Not(IsNull()), assetId: Not(IsNull()),

View file

@ -35,9 +35,9 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
// thumbnails // thumbnails
[JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION,
[JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_PREVIEW]: QueueName.THUMBNAIL_GENERATION,
[JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
[JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION,
[JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
// metadata // metadata

View file

@ -44,13 +44,13 @@ const _getAsset_1 = () => {
asset_1.deviceId = 'device_id_1'; asset_1.deviceId = 'device_id_1';
asset_1.type = AssetType.VIDEO; asset_1.type = AssetType.VIDEO;
asset_1.originalPath = 'fake_path/asset_1.jpeg'; 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.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
asset_1.fileCreatedAt = 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.updatedAt = new Date('2022-06-19T23:41:36.910Z');
asset_1.isFavorite = false; asset_1.isFavorite = false;
asset_1.isArchived = false; asset_1.isArchived = false;
asset_1.webpPath = ''; asset_1.thumbnailPath = '';
asset_1.encodedVideoPath = ''; asset_1.encodedVideoPath = '';
asset_1.duration = '0:00:00.000000'; asset_1.duration = '0:00:00.000000';
asset_1.exifInfo = new ExifEntity(); asset_1.exifInfo = new ExifEntity();

View file

@ -247,16 +247,16 @@ export class AssetServiceV1 {
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
switch (format) { switch (format) {
case GetAssetThumbnailFormatEnum.WEBP: { case GetAssetThumbnailFormatEnum.WEBP: {
if (asset.webpPath) { if (asset.thumbnailPath) {
return asset.webpPath; return asset.thumbnailPath;
} }
this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
} }
case GetAssetThumbnailFormatEnum.JPEG: { case GetAssetThumbnailFormatEnum.JPEG: {
if (!asset.resizePath) { if (!asset.previewPath) {
throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); 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 * Serve file viewer on the web
*/ */
if (dto.isWeb && mimeType != 'image/gif') { if (dto.isWeb && mimeType != 'image/gif') {
if (!asset.resizePath) { if (!asset.previewPath) {
this.logger.error('Error serving IMAGE asset for web'); this.logger.error('Error serving IMAGE asset for web');
throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile'); 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; return asset.originalPath;
} }
if (asset.webpPath && asset.webpPath.length > 0) { if (asset.thumbnailPath && asset.thumbnailPath.length > 0) {
return asset.webpPath; return asset.thumbnailPath;
} }
if (!asset.resizePath) { if (!asset.previewPath) {
throw new Error('resizePath not set'); throw new Error('previewPath not set');
} }
return asset.resizePath; return asset.previewPath;
} }
private async getLibraryId(auth: AuthDto, libraryId?: string) { private async getLibraryId(auth: AuthDto, libraryId?: string) {

View file

@ -661,8 +661,8 @@ describe(AssetService.name, () => {
name: JobName.DELETE_FILES, name: JobName.DELETE_FILES,
data: { data: {
files: [ files: [
assetWithFace.webpPath, assetWithFace.thumbnailPath,
assetWithFace.resizePath, assetWithFace.previewPath,
assetWithFace.encodedVideoPath, assetWithFace.encodedVideoPath,
assetWithFace.sidecarPath, assetWithFace.sidecarPath,
assetWithFace.originalPath, assetWithFace.originalPath,
@ -745,8 +745,8 @@ describe(AssetService.name, () => {
name: JobName.DELETE_FILES, name: JobName.DELETE_FILES,
data: { data: {
files: [ files: [
assetStub.external.webpPath, assetStub.external.thumbnailPath,
assetStub.external.resizePath, assetStub.external.previewPath,
assetStub.external.encodedVideoPath, assetStub.external.encodedVideoPath,
assetStub.external.sidecarPath, assetStub.external.sidecarPath,
], ],
@ -828,9 +828,7 @@ describe(AssetService.name, () => {
it('should run the refresh thumbnails job', async () => { it('should run the refresh thumbnails job', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }), await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }),
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]);
{ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } },
]);
}); });
it('should run the transcode video', async () => { it('should run the transcode video', async () => {

View file

@ -399,7 +399,7 @@ export class AssetService {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } }); 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) { if (!fromExternal) {
files.push(asset.originalPath); files.push(asset.originalPath);
} }
@ -472,7 +472,7 @@ export class AssetService {
} }
case AssetJobName.REGENERATE_THUMBNAIL: { case AssetJobName.REGENERATE_THUMBNAIL: {
jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } }); jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id } });
break; break;
} }

View file

@ -95,13 +95,13 @@ export class AuditService {
break; break;
} }
case AssetPathType.JPEG_THUMBNAIL: { case AssetPathType.PREVIEW: {
await this.assetRepository.update({ id, resizePath: pathValue }); await this.assetRepository.update({ id, previewPath: pathValue });
break; break;
} }
case AssetPathType.WEBP_THUMBNAIL: { case AssetPathType.THUMBNAIL: {
await this.assetRepository.update({ id, webpPath: pathValue }); await this.assetRepository.update({ id, thumbnailPath: pathValue });
break; break;
} }
@ -174,8 +174,8 @@ export class AuditService {
const orphans: FileReportItemDto[] = []; const orphans: FileReportItemDto[] = [];
for await (const assets of pagination) { for await (const assets of pagination) {
assetCount += assets.length; assetCount += assets.length;
for (const { id, originalPath, resizePath, encodedVideoPath, webpPath, isExternal, checksum } of assets) { for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) {
for (const file of [originalPath, resizePath, encodedVideoPath, webpPath]) { for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) {
track(file); track(file);
} }
@ -191,14 +191,14 @@ export class AuditService {
) { ) {
orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath });
} }
if (resizePath && !hasFile(thumbFiles, resizePath)) { if (previewPath && !hasFile(thumbFiles, previewPath)) {
orphans.push({ ...entity, pathType: AssetPathType.JPEG_THUMBNAIL, pathValue: resizePath }); orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath });
} }
if (webpPath && !hasFile(thumbFiles, webpPath)) { if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) {
orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: webpPath }); orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath });
} }
if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) {
orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: encodedVideoPath }); orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath });
} }
} }
} }

View file

@ -279,7 +279,7 @@ describe(JobService.name, () => {
}, },
{ {
item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, 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' } }, item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } },
@ -290,24 +290,24 @@ describe(JobService.name, () => {
jobs: [], jobs: [],
}, },
{ {
item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } },
jobs: [JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL], 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: [ jobs: [
JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBNAIL,
JobName.GENERATE_THUMBHASH_THUMBNAIL, JobName.GENERATE_THUMBHASH,
JobName.SMART_SEARCH, JobName.SMART_SEARCH,
JobName.FACE_DETECTION, JobName.FACE_DETECTION,
JobName.VIDEO_CONVERSION, 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: [ jobs: [
JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBNAIL,
JobName.GENERATE_THUMBHASH_THUMBNAIL, JobName.GENERATE_THUMBHASH,
JobName.SMART_SEARCH, JobName.SMART_SEARCH,
JobName.FACE_DETECTION, JobName.FACE_DETECTION,
JobName.VIDEO_CONVERSION, JobName.VIDEO_CONVERSION,
@ -329,7 +329,7 @@ describe(JobService.name, () => {
for (const { item, jobs } of tests) { for (const { item, jobs } of tests) {
it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { 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') { if (item.data.id === 'asset-live-image') {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
} else { } else {

View file

@ -245,7 +245,7 @@ export class JobService {
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
if (item.data.source === 'upload') { 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; break;
} }
@ -259,10 +259,10 @@ export class JobService {
break; break;
} }
case JobName.GENERATE_JPEG_THUMBNAIL: { case JobName.GENERATE_PREVIEW: {
const jobs: JobItem[] = [ const jobs: JobItem[] = [
{ name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data }, { name: JobName.GENERATE_THUMBNAIL, data: item.data },
{ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: item.data }, { name: JobName.GENERATE_THUMBHASH, data: item.data },
]; ];
if (item.data.source === 'upload') { if (item.data.source === 'upload') {
@ -282,7 +282,7 @@ export class JobService {
break; break;
} }
case JobName.GENERATE_WEBP_THUMBNAIL: { case JobName.GENERATE_THUMBNAIL: {
if (item.data.source !== 'upload') { if (item.data.source !== 'upload') {
break; break;
} }

View file

@ -4,6 +4,7 @@ import { ExifEntity } from 'src/entities/exif.entity';
import { import {
AudioCodec, AudioCodec,
Colorspace, Colorspace,
ImageFormat,
SystemConfigKey, SystemConfigKey,
ToneMapping, ToneMapping,
TranscodeHWAccel, TranscodeHWAccel,
@ -78,7 +79,7 @@ describe(MediaService.name, () => {
expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(assetMock.getWithout).not.toHaveBeenCalled();
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.GENERATE_JPEG_THUMBNAIL, name: JobName.GENERATE_PREVIEW,
data: { id: assetStub.image.id }, data: { id: assetStub.image.id },
}, },
]); ]);
@ -136,7 +137,7 @@ describe(MediaService.name, () => {
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.GENERATE_JPEG_THUMBNAIL, name: JobName.GENERATE_PREVIEW,
data: { id: assetStub.image.id }, data: { id: assetStub.image.id },
}, },
]); ]);
@ -160,7 +161,7 @@ describe(MediaService.name, () => {
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.GENERATE_WEBP_THUMBNAIL, name: JobName.GENERATE_THUMBNAIL,
data: { id: assetStub.image.id }, data: { id: assetStub.image.id },
}, },
]); ]);
@ -184,7 +185,7 @@ describe(MediaService.name, () => {
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL);
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.GENERATE_THUMBHASH_THUMBNAIL, name: JobName.GENERATE_THUMBHASH,
data: { id: assetStub.image.id }, 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 () => { it('should skip thumbnail generation if asset not found', async () => {
assetMock.getByIds.mockResolvedValue([]); assetMock.getByIds.mockResolvedValue([]);
await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); await sut.handleGeneratePreview({ id: assetStub.image.id });
expect(mediaMock.resize).not.toHaveBeenCalled(); expect(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith(); expect(assetMock.update).not.toHaveBeenCalledWith();
}); });
@ -204,25 +205,29 @@ describe(MediaService.name, () => {
it('should skip video thumbnail generation if no video stream', async () => { it('should skip video thumbnail generation if no video stream', async () => {
mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams);
assetMock.getByIds.mockResolvedValue([assetStub.video]); 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(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith(); expect(assetMock.update).not.toHaveBeenCalledWith();
}); });
it('should generate a thumbnail for an image', async () => { it('should generate a thumbnail for an image', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); 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(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', { expect(mediaMock.resize).toHaveBeenCalledWith(
size: 1440, '/original/path.jpg',
format: 'jpeg', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
quality: 80, {
colorspace: Colorspace.SRGB, size: 1440,
}); format: ImageFormat.JPEG,
quality: 80,
colorspace: Colorspace.SRGB,
},
);
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id', 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([ assetMock.getByIds.mockResolvedValue([
{ ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, { ...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(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', { expect(mediaMock.resize).toHaveBeenCalledWith(
size: 1440, '/original/path.jpg',
format: 'jpeg', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
quality: 80, {
colorspace: Colorspace.P3, size: 1440,
}); format: ImageFormat.JPEG,
quality: 80,
colorspace: Colorspace.P3,
},
);
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id', 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 () => { it('should generate a thumbnail for a video', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p);
assetMock.getByIds.mockResolvedValue([assetStub.video]); 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(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
expect(mediaMock.transcode).toHaveBeenCalledWith( expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext', '/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'], inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
outputOptions: [ outputOptions: [
@ -266,19 +275,19 @@ describe(MediaService.name, () => {
); );
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id', 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 () => { it('should tonemap thumbnail for hdr video', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
assetMock.getByIds.mockResolvedValue([assetStub.video]); 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(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se');
expect(mediaMock.transcode).toHaveBeenCalledWith( expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext', '/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'], inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
outputOptions: [ outputOptions: [
@ -291,7 +300,7 @@ describe(MediaService.name, () => {
); );
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id', 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' }, { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '5000k' },
]); ]);
assetMock.getByIds.mockResolvedValue([assetStub.video]); assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); await sut.handleGeneratePreview({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith( expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext', '/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'], inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'],
outputOptions: [ outputOptions: [
@ -321,31 +330,35 @@ describe(MediaService.name, () => {
it('should run successfully', async () => { it('should run successfully', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); 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 () => { it('should skip thumbnail generation if asset not found', async () => {
assetMock.getByIds.mockResolvedValue([]); assetMock.getByIds.mockResolvedValue([]);
await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); await sut.handleGenerateThumbnail({ id: assetStub.image.id });
expect(mediaMock.resize).not.toHaveBeenCalled(); expect(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith(); expect(assetMock.update).not.toHaveBeenCalledWith();
}); });
it('should generate a thumbnail', async () => { it('should generate a thumbnail', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); 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', { expect(mediaMock.resize).toHaveBeenCalledWith(
format: 'webp', '/original/path.jpg',
size: 250, 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
quality: 80, {
colorspace: Colorspace.SRGB, format: ImageFormat.WEBP,
}); size: 250,
quality: 80,
colorspace: Colorspace.SRGB,
},
);
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id', 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([ assetMock.getByIds.mockResolvedValue([
{ ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, { ...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(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', { expect(mediaMock.resize).toHaveBeenCalledWith(
format: 'webp', '/original/path.jpg',
size: 250, 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
quality: 80, {
colorspace: Colorspace.P3, format: ImageFormat.WEBP,
}); size: 250,
quality: 80,
colorspace: Colorspace.P3,
},
);
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: 'asset-id', 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', () => { describe('handleGenerateThumbhashThumbnail', () => {
it('should skip thumbhash generation if asset not found', async () => { it('should skip thumbhash generation if asset not found', async () => {
assetMock.getByIds.mockResolvedValue([]); assetMock.getByIds.mockResolvedValue([]);
await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); await sut.handleGenerateThumbhash({ id: assetStub.image.id });
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
}); });
it('should skip thumbhash generation if resize path is missing', async () => { it('should skip thumbhash generation if resize path is missing', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
await sut.handleGenerateThumbhashThumbnail({ id: assetStub.noResizePath.id }); await sut.handleGenerateThumbhash({ id: assetStub.noResizePath.id });
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
}); });
@ -387,7 +404,7 @@ describe(MediaService.name, () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); 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(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });

View file

@ -1,5 +1,5 @@
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; 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 { SystemConfigCore } from 'src/cores/system-config.core';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity, AssetType } from 'src/entities/asset.entity'; import { AssetEntity, AssetType } from 'src/entities/asset.entity';
@ -7,6 +7,7 @@ import { AssetPathType } from 'src/entities/move.entity';
import { import {
AudioCodec, AudioCodec,
Colorspace, Colorspace,
ImageFormat,
TranscodeHWAccel, TranscodeHWAccel,
TranscodePolicy, TranscodePolicy,
TranscodeTarget, TranscodeTarget,
@ -81,15 +82,15 @@ export class MediaService {
const jobs: JobItem[] = []; const jobs: JobItem[] = [];
for (const asset of assets) { for (const asset of assets) {
if (!asset.resizePath || force) { if (!asset.previewPath || force) {
jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } });
continue; continue;
} }
if (!asset.webpPath) { if (!asset.thumbnailPath) {
jobs.push({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { id: asset.id } }); jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } });
} }
if (!asset.thumbhash) { 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> { async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
const { image } = await this.configCore.getConfig();
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL); await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat);
await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL); await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO); await this.storageCore.moveAssetVideo(asset);
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const resizePath = await this.generateThumbnail(asset, 'jpeg'); const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG);
await this.assetRepository.update({ id: asset.id, resizePath }); await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') { private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
const { thumbnail, ffmpeg } = await this.configCore.getConfig(); const { image, ffmpeg } = await this.configCore.getConfig();
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize; const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
const path = const path = StorageCore.getImagePath(asset, type, format);
format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset);
this.storageCore.ensureFolders(path); this.storageCore.ensureFolders(path);
switch (asset.type) { switch (asset.type) {
case AssetType.IMAGE: { case AssetType.IMAGE: {
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace; const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality }; const imageOptions = { format, size, colorspace, quality: image.quality };
await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions); await this.mediaRepository.resize(asset.originalPath, path, imageOptions);
break; break;
} }
@ -214,24 +215,24 @@ export class MediaService {
return path; return path;
} }
async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const webpPath = await this.generateThumbnail(asset, 'webp'); const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP);
await this.assetRepository.update({ id: asset.id, webpPath }); await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;
} }
async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise<JobStatus> { async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]); const [asset] = await this.assetRepository.getByIds([id]);
if (!asset?.resizePath) { if (!asset?.previewPath) {
return JobStatus.FAILED; 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 }); await this.assetRepository.update({ id: asset.id, thumbhash });
return JobStatus.SUCCESS; return JobStatus.SUCCESS;

View file

@ -53,9 +53,9 @@ export class MicroservicesService {
[JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data), [JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data),
[JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data), [JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data),
[JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data), [JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data),
[JobName.GENERATE_JPEG_THUMBNAIL]: (data) => this.mediaService.handleGenerateJpegThumbnail(data), [JobName.GENERATE_PREVIEW]: (data) => this.mediaService.handleGeneratePreview(data),
[JobName.GENERATE_WEBP_THUMBNAIL]: (data) => this.mediaService.handleGenerateWebpThumbnail(data), [JobName.GENERATE_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbnail(data),
[JobName.GENERATE_THUMBHASH_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbhashThumbnail(data), [JobName.GENERATE_THUMBHASH]: (data) => this.mediaService.handleGenerateThumbhash(data),
[JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data), [JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data),
[JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data), [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data),
[JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data), [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data),

View file

@ -645,7 +645,7 @@ describe(PersonService.name, () => {
expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( expect(machineLearningMock.detectFaces).toHaveBeenCalledWith(
'http://immich-machine-learning:3003', 'http://immich-machine-learning:3003',
{ {
imagePath: assetStub.image.resizePath, imagePath: assetStub.image.previewPath,
}, },
{ {
enabled: true, enabled: true,

View file

@ -23,6 +23,7 @@ import {
} from 'src/dtos/person.dto'; } from 'src/dtos/person.dto';
import { PersonPathType } from 'src/entities/move.entity'; import { PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { ImageFormat } from 'src/entities/system-config.entity';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@ -315,17 +316,17 @@ export class PersonService {
}, },
}; };
const [asset] = await this.assetRepository.getByIds([id], relations); 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; return JobStatus.FAILED;
} }
const faces = await this.machineLearningRepository.detectFaces( const faces = await this.machineLearningRepository.detectFaces(
machineLearning.url, machineLearning.url,
{ imagePath: asset.resizePath }, { imagePath: asset.previewPath },
machineLearning.facialRecognition, 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})` }))); this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` })));
if (faces.length > 0) { if (faces.length > 0) {
@ -470,7 +471,7 @@ export class PersonService {
} }
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> { 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) { if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
@ -496,7 +497,7 @@ export class PersonService {
} = face; } = face;
const [asset] = await this.assetRepository.getByIds([assetId]); const [asset] = await this.assetRepository.getByIds([assetId]);
if (!asset?.resizePath) { if (!asset?.previewPath) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
this.logger.verbose(`Cropping face for person: ${person.id}`); this.logger.verbose(`Cropping face for person: ${person.id}`);
@ -527,12 +528,12 @@ export class PersonService {
height: newHalfSize * 2, height: newHalfSize * 2,
}; };
const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions); const croppedOutput = await this.mediaRepository.crop(asset.previewPath, cropOptions);
const thumbnailOptions = { const thumbnailOptions = {
format: 'jpeg', format: ImageFormat.JPEG,
size: FACE_THUMBNAIL_SIZE, size: FACE_THUMBNAIL_SIZE,
colorspace: thumbnail.colorspace, colorspace: image.colorspace,
quality: thumbnail.quality, quality: image.quality,
} as const; } as const;
await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions); await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);

View file

@ -76,6 +76,9 @@ export class SearchService {
checksum = Buffer.from(dto.checksum, encoding); checksum = Buffer.from(dto.checksum, encoding);
} }
dto.previewPath ??= dto.resizePath;
dto.thumbnailPath ??= dto.webpPath;
const page = dto.page ?? 1; const page = dto.page ?? 1;
const size = dto.size || 250; const size = dto.size || 250;
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;

View file

@ -18,7 +18,7 @@ import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.r
const asset = { const asset = {
id: 'asset-1', id: 'asset-1',
resizePath: 'path/to/resize.ext', previewPath: 'path/to/resize.ext',
} as AssetEntity; } as AssetEntity;
describe(SmartInfoService.name, () => { describe(SmartInfoService.name, () => {
@ -94,7 +94,7 @@ describe(SmartInfoService.name, () => {
}); });
it('should skip assets without a resize path', async () => { it('should skip assets without a resize path', async () => {
const asset = { resizePath: '' } as AssetEntity; const asset = { previewPath: '' } as AssetEntity;
assetMock.getByIds.mockResolvedValue([asset]); assetMock.getByIds.mockResolvedValue([asset]);
await sut.handleEncodeClip({ id: asset.id }); await sut.handleEncodeClip({ id: asset.id });

View file

@ -83,13 +83,13 @@ export class SmartInfoService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.resizePath) { if (!asset.previewPath) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
const clipEmbedding = await this.machineLearning.encodeImage( const clipEmbedding = await this.machineLearning.encodeImage(
machineLearning.url, machineLearning.url,
{ imagePath: asset.resizePath }, { imagePath: asset.previewPath },
machineLearning.clip, machineLearning.clip,
); );

View file

@ -4,6 +4,7 @@ import {
AudioCodec, AudioCodec,
CQMode, CQMode,
Colorspace, Colorspace,
ImageFormat,
LogLevel, LogLevel,
SystemConfig, SystemConfig,
SystemConfigEntity, SystemConfigEntity,
@ -119,9 +120,11 @@ const updatedConfig = Object.freeze<SystemConfig>({
hashVerificationEnabled: true, hashVerificationEnabled: true,
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
}, },
thumbnail: { image: {
webpSize: 250, thumbnailFormat: ImageFormat.WEBP,
jpegSize: 1440, thumbnailSize: 250,
previewFormat: ImageFormat.JPEG,
previewSize: 1440,
quality: 80, quality: 80,
colorspace: Colorspace.P3, colorspace: Colorspace.P3,
}, },

View file

@ -58,7 +58,7 @@ export function searchAssetBuilder(
builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); 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)); builder.andWhere(_.omitBy(path, _.isUndefined));
if (options.originalFileName) { if (options.originalFileName) {

View file

@ -26,10 +26,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: 'upload/library/IMG_123.jpg', originalPath: 'upload/library/IMG_123.jpg',
resizePath: null, previewPath: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -62,10 +62,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: 'upload/library/IMG_456.jpg', 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'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: null, thumbnailPath: null,
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -102,10 +102,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: null, thumbhash: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -139,10 +139,10 @@ export const assetStub = {
ownerId: 'admin-id', ownerId: 'admin-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
resizePath: '/uploads/admin-id/thumbs/path.jpg', previewPath: '/uploads/admin-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/admin-id/webp/path.ext', thumbnailPath: '/uploads/admin-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -184,10 +184,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
resizePath: '/uploads/user-id/thumbs/path.jpg', previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -224,10 +224,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', 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'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -264,10 +264,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.jpg', originalPath: '/original/path.jpg',
resizePath: '/uploads/user-id/thumbs/path.jpg', previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -304,10 +304,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -344,10 +344,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2015-02-23T05:06:29.716Z'), createdAt: new Date('2015-02-23T05:06:29.716Z'),
@ -385,10 +385,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.VIDEO, type: AssetType.VIDEO,
webpPath: null, thumbnailPath: null,
thumbhash: null, thumbhash: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -456,10 +456,10 @@ export const assetStub = {
deviceId: 'device-id', deviceId: 'device-id',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
sidecarPath: null, sidecarPath: null,
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: null, thumbnailPath: null,
thumbhash: null, thumbhash: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-22T05:06:29.716Z'), createdAt: new Date('2023-02-22T05:06:29.716Z'),
@ -499,11 +499,11 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
thumbhash: null, thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: null, thumbnailPath: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: 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', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
thumbhash: null, thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: null, thumbnailPath: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: 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', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
thumbhash: null, thumbhash: null,
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: null, thumbnailPath: null,
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: 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', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/original/path.ext', originalPath: '/original/path.ext',
resizePath: '/uploads/user-id/thumbs/path.ext', previewPath: '/uploads/user-id/thumbs/path.ext',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.VIDEO, type: AssetType.VIDEO,
webpPath: null, thumbnailPath: null,
thumbhash: null, thumbhash: null,
encodedVideoPath: '/encoded/video/path.mp4', encodedVideoPath: '/encoded/video/path.mp4',
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -648,10 +648,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', 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'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),
@ -687,10 +687,10 @@ export const assetStub = {
ownerId: 'user-id', ownerId: 'user-id',
deviceId: 'device-id', deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg', 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'), checksum: Buffer.from('file hash', 'utf8'),
type: AssetType.IMAGE, type: AssetType.IMAGE,
webpPath: '/uploads/user-id/webp/path.ext', thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'), thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null, encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'), createdAt: new Date('2023-02-23T05:06:29.716Z'),

View file

@ -199,7 +199,7 @@ export const sharedLinkStub = {
deviceId: 'device_id_1', deviceId: 'device_id_1',
type: AssetType.VIDEO, type: AssetType.VIDEO,
originalPath: 'fake_path/jpeg', originalPath: 'fake_path/jpeg',
resizePath: '', previewPath: '',
checksum: Buffer.from('file hash', 'utf8'), checksum: Buffer.from('file hash', 'utf8'),
fileModifiedAt: today, fileModifiedAt: today,
fileCreatedAt: today, fileCreatedAt: today,
@ -219,7 +219,7 @@ export const sharedLinkStub = {
objects: ['a', 'b', 'c'], objects: ['a', 'b', 'c'],
asset: null as any, asset: null as any,
}, },
webpPath: '', thumbnailPath: '',
thumbhash: null, thumbhash: null,
encodedVideoPath: '', encodedVideoPath: '',
duration: null, duration: null,

View file

@ -25,10 +25,10 @@
<form autocomplete="off" on:submit|preventDefault> <form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4"> <div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSelect <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." 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 number
bind:value={config.thumbnail.webpSize} bind:value={config.image.thumbnailSize}
options={[ options={[
{ value: 1080, text: '1080p' }, { value: 1080, text: '1080p' },
{ value: 720, text: '720p' }, { value: 720, text: '720p' },
@ -37,15 +37,15 @@
{ value: 200, text: '200p' }, { value: 200, text: '200p' },
]} ]}
name="resolution" name="resolution"
isEdited={config.thumbnail.webpSize !== savedConfig.thumbnail.webpSize} isEdited={config.image.thumbnailSize !== savedConfig.image.thumbnailSize}
{disabled} {disabled}
/> />
<SettingSelect <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." 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 number
bind:value={config.thumbnail.jpegSize} bind:value={config.image.previewSize}
options={[ options={[
{ value: 2160, text: '4K' }, { value: 2160, text: '4K' },
{ value: 1440, text: '1440p' }, { value: 1440, text: '1440p' },
@ -53,31 +53,31 @@
{ value: 720, text: '720p' }, { value: 720, text: '720p' },
]} ]}
name="resolution" name="resolution"
isEdited={config.thumbnail.jpegSize !== savedConfig.thumbnail.jpegSize} isEdited={config.image.previewSize !== savedConfig.image.previewSize}
{disabled} {disabled}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label="QUALITY" label="QUALITY"
desc="Thumbnail quality from 1-100. Higher is better for quality but produces larger files." desc="Image quality from 1-100. Higher is better for quality but produces larger files."
bind:value={config.thumbnail.quality} bind:value={config.image.quality}
isEdited={config.thumbnail.quality !== savedConfig.thumbnail.quality} isEdited={config.image.quality !== savedConfig.image.quality}
/> />
<SettingSwitch <SettingSwitch
title="PREFER WIDE GAMUT" 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." 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} checked={config.image.colorspace === Colorspace.P3}
on:toggle={(e) => (config.thumbnail.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
isEdited={config.thumbnail.colorspace !== savedConfig.thumbnail.colorspace} isEdited={config.image.colorspace !== savedConfig.image.colorspace}
/> />
</div> </div>
<div class="ml-4"> <div class="ml-4">
<SettingButtonsRow <SettingButtonsRow
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['thumbnail'] })} on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['image'] })}
on:save={() => dispatch('save', { thumbnail: config.thumbnail })} on:save={() => dispatch('save', { image: config.image })}
showResetToDefault={!isEqual(savedConfig, defaultConfig)} showResetToDefault={!isEqual(savedConfig, defaultConfig)}
{disabled} {disabled}
/> />

View file

@ -13,7 +13,7 @@
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; 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 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 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 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 UserSettings from '$lib/components/admin-page/settings/user-settings/user-settings.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
@ -43,7 +43,7 @@
| typeof ServerSettings | typeof ServerSettings
| typeof StorageTemplateSettings | typeof StorageTemplateSettings
| typeof ThemeSettings | typeof ThemeSettings
| typeof ThumbnailSettings | typeof ImageSettings
| typeof TrashSettings | typeof TrashSettings
| typeof NewVersionCheckSettings | typeof NewVersionCheckSettings
| typeof FFmpegSettings | typeof FFmpegSettings
@ -64,6 +64,12 @@
subtitle: string; subtitle: string;
key: string; key: string;
}> = [ }> = [
{
item: ImageSettings,
title: 'Image Settings',
subtitle: 'Manage the quality and resolution of generated images',
key: 'image',
},
{ {
item: JobSettings, item: JobSettings,
title: 'Job Settings', title: 'Job Settings',
@ -124,12 +130,6 @@
subtitle: 'Manage customization of the Immich web interface', subtitle: 'Manage customization of the Immich web interface',
key: 'theme', key: 'theme',
}, },
{
item: ThumbnailSettings,
title: 'Thumbnail Settings',
subtitle: 'Manage the resolution of thumbnail sizes',
key: 'thumbnail',
},
{ {
item: TrashSettings, item: TrashSettings,
title: 'Trash Settings', title: 'Trash Settings',