diff --git a/server/src/migrations/1715435221124-MotionAssetExtensionMP4.ts b/server/src/migrations/1715435221124-MotionAssetExtensionMP4.ts new file mode 100644 index 0000000000..f037ba1fb0 --- /dev/null +++ b/server/src/migrations/1715435221124-MotionAssetExtensionMP4.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MotionAssetExtensionMP41715435221124 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "assets" SET "originalFileName" = regexp_replace("originalFileName", '\\.[a-zA-Z0-9]+$', '.mp4') WHERE "originalPath" LIKE '%.mp4' AND "isVisible" = false`, + ); + } + + public async down(): Promise {} +} diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index da90b83794..59294bdcfc 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -356,7 +356,7 @@ describe(MetadataService.name, () => { }); it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => { - assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); metadataMock.readTags.mockResolvedValue({ Directory: 'foo/bar/', MotionPhotoVideo: new BinaryField(0, ''), @@ -372,23 +372,23 @@ describe(MetadataService.name, () => { const video = randomBytes(512); metadataMock.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoStillAsset.originalPath, + assetStub.livePhotoWithOriginalFileName.originalPath, 'MotionPhotoVideo', ); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]); expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { - id: assetStub.livePhotoStillAsset.id, + id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); }); it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => { - assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); metadataMock.readTags.mockResolvedValue({ Directory: 'foo/bar/', EmbeddedVideoFile: new BinaryField(0, ''), @@ -401,23 +401,23 @@ describe(MetadataService.name, () => { const video = randomBytes(512); metadataMock.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoStillAsset.originalPath, + assetStub.livePhotoWithOriginalFileName.originalPath, 'EmbeddedVideoFile', ); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]); expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { - id: assetStub.livePhotoStillAsset.id, + id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); }); it('should extract the motion photo video from the XMP directory entry ', async () => { - assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]); + assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]); metadataMock.readTags.mockResolvedValue({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -431,20 +431,23 @@ describe(MetadataService.name, () => { const video = randomBytes(512); storageMock.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]); - expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object)); + await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]); + expect(storageMock.readFile).toHaveBeenCalledWith( + assetStub.livePhotoWithOriginalFileName.originalPath, + expect.any(Object), + ); expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { - id: assetStub.livePhotoStillAsset.id, + id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, }); }); it('should delete old motion photo video assets if they do not match what is extracted', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); + assetMock.getByIds.mockResolvedValue([assetStub.livePhotoWithOriginalFileName]); metadataMock.readTags.mockResolvedValue({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -457,10 +460,10 @@ describe(MetadataService.name, () => { const video = randomBytes(512); storageMock.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); expect(jobMock.queue).toHaveBeenNthCalledWith(1, { name: JobName.ASSET_DELETION, - data: { id: assetStub.livePhotoStillAsset.livePhotoVideoId }, + data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId }, }); expect(jobMock.queue).toHaveBeenNthCalledWith(2, { name: JobName.METADATA_EXTRACTION, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 7d7e2f91ac..a0b46ccbaa 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -435,7 +435,7 @@ export class MetadataService { checksum, ownerId: asset.ownerId, originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), - originalFileName: asset.originalFileName, + originalFileName: `${path.parse(asset.originalFileName).name}.mp4`, isVisible: false, deviceAssetId: 'NONE', deviceId: 'NONE', diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index e71094f456..56aeed9d81 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -486,6 +486,22 @@ export const assetStub = { }, } as AssetEntity), + livePhotoWithOriginalFileName: Object.freeze({ + id: 'live-photo-still-asset', + originalPath: fileStub.livePhotoStill.originalPath, + originalFileName: fileStub.livePhotoStill.originalName, + ownerId: authStub.user1.user.id, + type: AssetType.IMAGE, + livePhotoVideoId: 'live-photo-motion-asset123', + isVisible: true, + fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + exifInfo: { + fileSizeInByte: 25_000, + timeZone: `America/New_York`, + }, + } as AssetEntity), + withLocation: Object.freeze({ id: 'asset-with-favorite-id', deviceAssetId: 'device-asset-id',