diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index fd2c3e388c..2150e494c9 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -439,7 +439,7 @@ describe(MetadataService.name, () => { }); cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); assetMock.getByChecksum.mockResolvedValue(null); - assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset); + assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset })); await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); expect(jobMock.queue).toHaveBeenNthCalledWith(2, { @@ -467,6 +467,30 @@ describe(MetadataService.name, () => { expect(jobMock.queue).toHaveBeenCalledTimes(0); }); + it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]); + metadataMock.readTags.mockResolvedValue({ + Directory: 'foo/bar/', + MotionPhoto: 1, + MicroVideo: 1, + MicroVideoOffset: 1, + }); + cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + assetMock.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true }); + const video = randomBytes(512); + storageMock.readFile.mockResolvedValue(video); + + await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + expect(assetMock.update).toHaveBeenNthCalledWith(1, { + id: assetStub.livePhotoMotionAsset.id, + isVisible: false, + }); + expect(assetMock.update).toHaveBeenNthCalledWith(2, { + id: assetStub.livePhotoStillAsset.id, + livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + }); + }); + it('should save all metadata', async () => { const tags: ImmichTags = { BitsPerSample: 1, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 327bd7b665..379e034884 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -412,6 +412,12 @@ export class MetadataService { 'base64', )} already exists in the repository`, ); + + // Hide the motion photo video asset if it's not already hidden to prepare for linking + if (motionAsset.isVisible) { + await this.assetRepository.update({ id: motionAsset.id, isVisible: false }); + this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`); + } } else { // We create a UUID in advance so that each extracted video can have a unique filename // (allowing us to delete old ones if necessary) @@ -438,11 +444,14 @@ export class MetadataService { this.storageCore.ensureFolders(motionPath); await this.storageRepository.writeFile(motionAsset.originalPath, video); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } }); - await this.assetRepository.update({ id: asset.id, livePhotoVideoId: motionAsset.id }); + } + if (asset.livePhotoVideoId !== motionAsset.id) { + await this.assetRepository.update({ id: asset.id, livePhotoVideoId: motionAsset.id }); // If the asset already had an associated livePhotoVideo, delete it, because // its checksum doesn't match the checksum of the motionAsset we just extracted - // (if it did, getByChecksum() would've returned non-null) + // (if it did, getByChecksum() would've returned a motionAsset with the same ID as livePhotoVideoId) + // note asset.livePhotoVideoId is not motionAsset.id yet if (asset.livePhotoVideoId) { await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } }); this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`);