From b190423d96384c1e64c1fe22faf82c77b3d3de35 Mon Sep 17 00:00:00 2001 From: Nikhil Alapati <35281285+NikhilAlapati@users.noreply.github.com> Date: Mon, 5 Jan 2026 07:26:45 -0800 Subject: [PATCH] fix(server): migrate motion part of live photo (#24688) Co-authored-by: Nikhil Alapati --- .../services/storage-template.service.spec.ts | 33 +++++++++++++++++++ .../src/services/storage-template.service.ts | 9 +++++ 2 files changed, 42 insertions(+) diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index c1898f8f12..0b5d538cea 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -616,6 +616,39 @@ describe(StorageTemplateService.name, () => { ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); + + it('should migrate live photo motion video alongside the still image', async () => { + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; + const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([stillAsset])); + mocks.user.getList.mockResolvedValue([userStub.user1]); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset); + + mocks.move.create.mockResolvedValueOnce({ + id: '123', + entityId: stillAsset.id, + pathType: AssetPathType.Original, + oldPath: stillAsset.originalPath, + newPath: newStillPicturePath, + }); + + mocks.move.create.mockResolvedValueOnce({ + id: '124', + entityId: motionAsset.id, + pathType: AssetPathType.Original, + oldPath: motionAsset.originalPath, + newPath: newMotionPicturePath, + }); + + await sut.handleMigration(); + + expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); + expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(motionAsset.id); + expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, originalPath: newStillPicturePath }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath }); + }); }); describe('file rename correctness', () => { diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index bbf5a8a6bf..cd641d7036 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -188,6 +188,15 @@ export class StorageTemplateService extends BaseService { const storageLabel = user?.storageLabel || null; const filename = asset.originalFileName || asset.id; await this.moveAsset(asset, { storageLabel, filename }); + + // move motion part of live photo + if (asset.livePhotoVideoId) { + const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId); + if (livePhotoVideo) { + const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath); + await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }); + } + } } this.logger.debug('Cleaning up empty directories...');