mirror of
https://github.com/immich-app/immich.git
synced 2025-01-17 01:06:46 +01:00
fix(server): use correct file extension for motion photo videos (#8659)
* fix(server): use mp4 file extension for motion photo videos in archive download * always use mp4 for videos * get file extension from originalPath * remove console log * store motion assets with mp4 extension * add migration * set originalFileName for live photo asset stubs * leave down migration empty * only set originalFileName for livePhotoStillAsset * use separate stub * shorter stub name
This commit is contained in:
parent
7168707395
commit
f197f5d530
4 changed files with 49 additions and 19 deletions
|
@ -0,0 +1,11 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class MotionAssetExtensionMP41715435221124 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
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<void> {}
|
||||||
|
}
|
|
@ -356,7 +356,7 @@ describe(MetadataService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
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({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhotoVideo: new BinaryField(0, ''),
|
MotionPhotoVideo: new BinaryField(0, ''),
|
||||||
|
@ -372,23 +372,23 @@ describe(MetadataService.name, () => {
|
||||||
const video = randomBytes(512);
|
const video = randomBytes(512);
|
||||||
metadataMock.extractBinaryTag.mockResolvedValue(video);
|
metadataMock.extractBinaryTag.mockResolvedValue(video);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||||
expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith(
|
expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith(
|
||||||
assetStub.livePhotoStillAsset.originalPath,
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||||
'MotionPhotoVideo',
|
'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(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoWithOriginalFileName.id,
|
||||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
|
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({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
EmbeddedVideoFile: new BinaryField(0, ''),
|
EmbeddedVideoFile: new BinaryField(0, ''),
|
||||||
|
@ -401,23 +401,23 @@ describe(MetadataService.name, () => {
|
||||||
const video = randomBytes(512);
|
const video = randomBytes(512);
|
||||||
metadataMock.extractBinaryTag.mockResolvedValue(video);
|
metadataMock.extractBinaryTag.mockResolvedValue(video);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||||
expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith(
|
expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith(
|
||||||
assetStub.livePhotoStillAsset.originalPath,
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||||
'EmbeddedVideoFile',
|
'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(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoWithOriginalFileName.id,
|
||||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
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({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
|
@ -431,20 +431,23 @@ describe(MetadataService.name, () => {
|
||||||
const video = randomBytes(512);
|
const video = randomBytes(512);
|
||||||
storageMock.readFile.mockResolvedValue(video);
|
storageMock.readFile.mockResolvedValue(video);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id]);
|
||||||
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
expect(storageMock.readFile).toHaveBeenCalledWith(
|
||||||
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
||||||
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512);
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoWithOriginalFileName.id,
|
||||||
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
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({
|
metadataMock.readTags.mockResolvedValue({
|
||||||
Directory: 'foo/bar/',
|
Directory: 'foo/bar/',
|
||||||
MotionPhoto: 1,
|
MotionPhoto: 1,
|
||||||
|
@ -457,10 +460,10 @@ describe(MetadataService.name, () => {
|
||||||
const video = randomBytes(512);
|
const video = randomBytes(512);
|
||||||
storageMock.readFile.mockResolvedValue(video);
|
storageMock.readFile.mockResolvedValue(video);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
||||||
expect(jobMock.queue).toHaveBeenNthCalledWith(1, {
|
expect(jobMock.queue).toHaveBeenNthCalledWith(1, {
|
||||||
name: JobName.ASSET_DELETION,
|
name: JobName.ASSET_DELETION,
|
||||||
data: { id: assetStub.livePhotoStillAsset.livePhotoVideoId },
|
data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId },
|
||||||
});
|
});
|
||||||
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
|
|
|
@ -435,7 +435,7 @@ export class MetadataService {
|
||||||
checksum,
|
checksum,
|
||||||
ownerId: asset.ownerId,
|
ownerId: asset.ownerId,
|
||||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||||
originalFileName: asset.originalFileName,
|
originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
deviceAssetId: 'NONE',
|
deviceAssetId: 'NONE',
|
||||||
deviceId: 'NONE',
|
deviceId: 'NONE',
|
||||||
|
|
16
server/test/fixtures/asset.stub.ts
vendored
16
server/test/fixtures/asset.stub.ts
vendored
|
@ -486,6 +486,22 @@ export const assetStub = {
|
||||||
},
|
},
|
||||||
} as AssetEntity),
|
} 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<AssetEntity>({
|
withLocation: Object.freeze<AssetEntity>({
|
||||||
id: 'asset-with-favorite-id',
|
id: 'asset-with-favorite-id',
|
||||||
deviceAssetId: 'device-asset-id',
|
deviceAssetId: 'device-asset-id',
|
||||||
|
|
Loading…
Reference in a new issue