diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index 7c2db418a4..d980ccb676 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -183,7 +183,7 @@ from where "assets"."id" = $1 --- AssetJobRepository.getAlbumThumbnailFile +-- AssetJobRepository.getAlbumThumbnailFiles select "asset_files"."id", "asset_files"."path", @@ -192,6 +192,32 @@ from "asset_files" where "asset_files"."assetId" = $1 + and "asset_files"."type" = $2 + +-- AssetJobRepository.getForClipEncoding +select + "assets"."id", + "assets"."isVisible", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_files"."id", + "asset_files"."path", + "asset_files"."type" + from + "asset_files" + where + "asset_files"."assetId" = "assets"."id" + and "asset_files"."type" = $1 + ) as agg + ) as "files" +from + "assets" +where + "assets"."id" = $2 -- AssetJobRepository.getForStorageTemplateJob select diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 480972a8ae..9ed40050d8 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -117,9 +117,24 @@ export class AssetJobRepository { .executeTakeFirst(); } + @GenerateSql({ params: [DummyValue.UUID, AssetFileType.THUMBNAIL] }) + getAlbumThumbnailFiles(id: string, fileType?: AssetFileType) { + return this.db + .selectFrom('asset_files') + .select(columns.assetFiles) + .where('asset_files.assetId', '=', id) + .$if(!!fileType, (qb) => qb.where('asset_files.type', '=', fileType!)) + .execute(); + } + @GenerateSql({ params: [DummyValue.UUID] }) - getAlbumThumbnailFile(id: string) { - return this.db.selectFrom('asset_files').select(columns.assetFiles).where('asset_files.assetId', '=', id).execute(); + getForClipEncoding(id: string) { + return this.db + .selectFrom('assets') + .select(['assets.id', 'assets.isVisible']) + .select((eb) => withFiles(eb, AssetFileType.PREVIEW)) + .where('assets.id', '=', id) + .executeTakeFirst(); } private storageTemplateAssetQuery() { diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 45663b4aa2..85e425b11f 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -412,11 +412,12 @@ describe(NotificationService.name, () => { }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue([]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.assetJob.getAlbumThumbnailFile).toHaveBeenCalledWith( + expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + AssetFileType.THUMBNAIL, ); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, @@ -440,13 +441,14 @@ describe(NotificationService.name, () => { }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue([ + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ { id: '1', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' }, ]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.assetJob.getAlbumThumbnailFile).toHaveBeenCalledWith( + expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + AssetFileType.THUMBNAIL, ); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, @@ -470,11 +472,12 @@ describe(NotificationService.name, () => { }); mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue(assetStub.image.files); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(mocks.assetJob.getAlbumThumbnailFile).toHaveBeenCalledWith( + expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + AssetFileType.THUMBNAIL, ); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, @@ -506,7 +509,7 @@ describe(NotificationService.name, () => { }); mocks.user.get.mockResolvedValueOnce(userStub.user1); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue([]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); @@ -528,7 +531,7 @@ describe(NotificationService.name, () => { ], }); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue([]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); @@ -550,7 +553,7 @@ describe(NotificationService.name, () => { ], }); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue([]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); @@ -564,7 +567,7 @@ describe(NotificationService.name, () => { }); mocks.user.get.mockResolvedValue(userStub.user1); mocks.notification.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFile.mockResolvedValue([]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); await sut.handleAlbumUpdate({ id: '', recipientIds: [userStub.user1.id] }); expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 9396681bfe..2c4cc76756 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -6,7 +6,6 @@ import { ArgOf } from 'src/repositories/event.repository'; import { EmailTemplate } from 'src/repositories/notification.repository'; import { BaseService } from 'src/services/base.service'; import { EmailImageAttachment, IEntityJob, INotifyAlbumUpdateJob, JobItem, JobOf } from 'src/types'; -import { getAssetFile } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; import { getExternalDomain } from 'src/utils/misc'; import { isEqualObject } from 'src/utils/object'; @@ -398,19 +397,18 @@ export class NotificationService extends BaseService { return; } - const albumThumbnailFiles = await this.assetJobRepository.getAlbumThumbnailFile(album.albumThumbnailAssetId); - if (albumThumbnailFiles.length === 0) { - return; - } + const albumThumbnailFiles = await this.assetJobRepository.getAlbumThumbnailFiles( + album.albumThumbnailAssetId, + AssetFileType.THUMBNAIL, + ); - const thumbnailFile = getAssetFile(albumThumbnailFiles, AssetFileType.THUMBNAIL); - if (!thumbnailFile) { + if (albumThumbnailFiles.length !== 1) { return; } return { - filename: `album-thumbnail${getFilenameExtension(thumbnailFile.path)}`, - path: thumbnailFile.path, + filename: `album-thumbnail${getFilenameExtension(albumThumbnailFiles[0].path)}`, + path: albumThumbnailFiles[0].path, cid: 'album-thumbnail', }; } diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index aef83a813d..df26e69108 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -264,7 +264,7 @@ describe(SmartInfoService.name, () => { }); it('should skip assets without a resize path', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.noResizePath]); + mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.noResizePath, files: [] }); expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED); @@ -274,6 +274,7 @@ describe(SmartInfoService.name, () => { it('should save the returned objects', async () => { mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); + mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); @@ -286,7 +287,10 @@ describe(SmartInfoService.name, () => { }); it('should skip invisible assets', async () => { - mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + mocks.assetJob.getForClipEncoding.mockResolvedValue({ + ...assetStub.livePhotoMotionAsset, + files: [assetStub.image.files[1]], + }); expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); @@ -295,7 +299,7 @@ describe(SmartInfoService.name, () => { }); it('should fail if asset could not be found', async () => { - mocks.asset.getByIds.mockResolvedValue([]); + mocks.assetJob.getForClipEncoding.mockResolvedValue(void 0); expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.FAILED); @@ -306,6 +310,7 @@ describe(SmartInfoService.name, () => { it('should wait for database', async () => { mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); mocks.database.isBusy.mockReturnValue(true); + mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 1909d2bb99..411114eb17 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -2,12 +2,11 @@ import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; -import { AssetFileType, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; +import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { WithoutProperty } from 'src/repositories/asset.repository'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; -import { getAssetFile } from 'src/utils/asset.util'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -107,8 +106,8 @@ export class SmartInfoService extends BaseService { return JobStatus.SKIPPED; } - const [asset] = await this.assetRepository.getByIds([id], { files: true }); - if (!asset) { + const asset = await this.assetJobRepository.getForClipEncoding(id); + if (!asset || asset.files.length !== 1) { return JobStatus.FAILED; } @@ -116,14 +115,9 @@ export class SmartInfoService extends BaseService { return JobStatus.SKIPPED; } - const previewFile = getAssetFile(asset.files, AssetFileType.PREVIEW); - if (!previewFile) { - return JobStatus.FAILED; - } - const embedding = await this.machineLearningRepository.encodeImage( machineLearning.urls, - previewFile.path, + asset.files[0].path, machineLearning.clip, );