diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 1b1adcd573..c6301c7c33 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -225,6 +225,15 @@ describe(MediaService.name, () => { expect(assetMock.update).not.toHaveBeenCalledWith(); }); + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(mediaMock.resize).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalledWith(); + }); + it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]); assetMock.getByIds.mockResolvedValue([assetStub.image]); @@ -353,6 +362,15 @@ describe(MediaService.name, () => { expect(assetMock.update).not.toHaveBeenCalledWith(); }); + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(mediaMock.resize).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalledWith(); + }); + it.each(Object.values(ImageFormat))( 'should generate a %s thumbnail for an image when specified', async (format) => { @@ -410,6 +428,15 @@ describe(MediaService.name, () => { expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalledWith(); + }); + it('should generate a thumbhash', async () => { const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); assetMock.getByIds.mockResolvedValue([assetStub.image]); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 47fa31abcc..ca72b6cbdd 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -77,7 +77,7 @@ export class MediaService { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination) + ? this.assetRepository.getAll(pagination, { isVisible: true }) : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL); }); @@ -178,6 +178,10 @@ export class MediaService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat); await this.assetRepository.update({ id: asset.id, previewPath }); return JobStatus.SUCCESS; @@ -230,6 +234,10 @@ export class MediaService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); await this.assetRepository.update({ id: asset.id, thumbnailPath }); return JobStatus.SUCCESS; @@ -237,7 +245,15 @@ export class MediaService { async handleGenerateThumbhash({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); - if (!asset?.previewPath) { + if (!asset) { + return JobStatus.FAILED; + } + + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + + if (!asset.previewPath) { return JobStatus.FAILED; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 2cd3cd88a9..721f2586ee 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -292,7 +292,12 @@ export class PersonService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true, withArchived: true }) + ? this.assetRepository.getAll(pagination, { + orderDirection: 'DESC', + withFaces: true, + withArchived: true, + isVisible: true, + }) : this.assetRepository.getWithout(pagination, WithoutProperty.FACES); }); @@ -322,6 +327,10 @@ export class PersonService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + const faces = await this.machineLearningRepository.detectFaces( machineLearning.url, { imagePath: asset.previewPath }, diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 4d85c00253..8dedcb5c5f 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,8 +1,7 @@ -import { AssetEntity } from 'src/entities/asset.entity'; import { SystemConfigKey } from 'src/entities/system-config.entity'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; -import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; @@ -19,11 +18,6 @@ import { newSearchRepositoryMock } from 'test/repositories/search.repository.moc import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock'; import { Mocked } from 'vitest'; -const asset = { - id: 'asset-1', - previewPath: 'path/to/resize.ext', -} as AssetEntity; - describe(SmartInfoService.name, () => { let sut: SmartInfoService; let assetMock: Mocked; @@ -44,7 +38,7 @@ describe(SmartInfoService.name, () => { loggerMock = newLoggerRepositoryMock(); sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock); - assetMock.getByIds.mockResolvedValue([asset]); + assetMock.getByIds.mockResolvedValue([assetStub.image]); }); it('should work', () => { @@ -92,17 +86,16 @@ describe(SmartInfoService.name, () => { it('should do nothing if machine learning is disabled', async () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]); - await sut.handleEncodeClip({ id: '123' }); + expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED); expect(assetMock.getByIds).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled(); }); it('should skip assets without a resize path', async () => { - const asset = { previewPath: '' } as AssetEntity; - assetMock.getByIds.mockResolvedValue([asset]); + assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); - await sut.handleEncodeClip({ id: asset.id }); + expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED); expect(searchMock.upsert).not.toHaveBeenCalled(); expect(machineMock.encodeImage).not.toHaveBeenCalled(); @@ -111,14 +104,23 @@ describe(SmartInfoService.name, () => { it('should save the returned objects', async () => { machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]); - await sut.handleEncodeClip({ id: asset.id }); + expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS); expect(machineMock.encodeImage).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - { imagePath: 'path/to/resize.ext' }, + { imagePath: assetStub.image.previewPath }, { enabled: true, modelName: 'ViT-B-32__openai' }, ); - expect(searchMock.upsert).toHaveBeenCalledWith('asset-1', [0.01, 0.02, 0.03]); + expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]); + }); + + it('should skip invisible assets', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + + expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + + expect(machineMock.encodeImage).not.toHaveBeenCalled(); + expect(searchMock.upsert).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 9de5edbd88..929d15beca 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -60,7 +60,7 @@ export class SmartInfoService { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination) + ? this.assetRepository.getAll(pagination, { isVisible: true }) : this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH); }); @@ -84,6 +84,10 @@ export class SmartInfoService { return JobStatus.FAILED; } + if (!asset.isVisible) { + return JobStatus.SKIPPED; + } + if (!asset.previewPath) { return JobStatus.FAILED; }