1
1
import { AssetType , ExifEntity , SystemConfigKey } from '@app/infra/entities' ;
2
2
import {
3
3
assetStub ,
4
+ fileStub ,
4
5
newAlbumRepositoryMock ,
5
6
newAssetRepositoryMock ,
6
7
newCommunicationRepositoryMock ,
@@ -16,6 +17,7 @@ import {
16
17
probeStub ,
17
18
} from '@test' ;
18
19
import { randomBytes } from 'crypto' ;
20
+ import { BinaryField } from 'exiftool-vendored' ;
19
21
import { Stats } from 'fs' ;
20
22
import { constants } from 'fs/promises' ;
21
23
import { when } from 'jest-when' ;
@@ -343,61 +345,127 @@ describe(MetadataService.name, () => {
343
345
) ;
344
346
} ) ;
345
347
346
- it ( 'should apply motion photos' , async ( ) => {
348
+ it ( 'should extract the MotionPhotoVideo tag from Samsung HEIC motion photos' , async ( ) => {
347
349
assetMock . getByIds . mockResolvedValue ( [ { ...assetStub . livePhotoStillAsset , livePhotoVideoId : null } ] ) ;
348
350
metadataMock . readTags . mockResolvedValue ( {
349
351
Directory : 'foo/bar/' ,
350
- MotionPhoto : 1 ,
351
- MicroVideo : 1 ,
352
- MicroVideoOffset : 1 ,
352
+ MotionPhotoVideo : new BinaryField ( 0 , '' ) ,
353
+ // The below two are included to ensure that the MotionPhotoVideo tag is extracted
354
+ // instead of the EmbeddedVideoFile, since HEIC MotionPhotos include both
355
+ EmbeddedVideoFile : new BinaryField ( 0 , '' ) ,
356
+ EmbeddedVideoType : 'MotionPhoto_Data' ,
353
357
} ) ;
354
- storageMock . readFile . mockResolvedValue ( randomBytes ( 512 ) ) ;
355
358
cryptoRepository . hashSha1 . mockReturnValue ( randomBytes ( 512 ) ) ;
356
- assetMock . getByChecksum . mockResolvedValue ( assetStub . livePhotoMotionAsset ) ;
359
+ assetMock . getByChecksum . mockResolvedValue ( null ) ;
360
+ assetMock . create . mockResolvedValue ( assetStub . livePhotoMotionAsset ) ;
361
+ cryptoRepository . randomUUID . mockReturnValue ( fileStub . livePhotoMotion . uuid ) ;
362
+ const video = randomBytes ( 512 ) ;
363
+ metadataMock . extractBinaryTag . mockResolvedValue ( video ) ;
357
364
358
365
await sut . handleMetadataExtraction ( { id : assetStub . livePhotoStillAsset . id } ) ;
366
+ expect ( metadataMock . extractBinaryTag ) . toHaveBeenCalledWith (
367
+ assetStub . livePhotoStillAsset . originalPath ,
368
+ 'MotionPhotoVideo' ,
369
+ ) ;
359
370
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . livePhotoStillAsset . id ] ) ;
360
- expect ( storageMock . readFile ) . toHaveBeenCalledWith ( assetStub . livePhotoStillAsset . originalPath , expect . any ( Object ) ) ;
361
- expect ( assetMock . save ) . toHaveBeenCalledWith ( {
371
+ expect ( assetMock . create ) . toHaveBeenCalled ( ) ; // This could have arguments added
372
+ expect ( storageMock . writeFile ) . toHaveBeenCalledWith ( assetStub . livePhotoMotionAsset . originalPath , video ) ;
373
+ expect ( assetMock . save ) . toHaveBeenNthCalledWith ( 1 , {
362
374
id : assetStub . livePhotoStillAsset . id ,
363
- livePhotoVideoId : assetStub . livePhotoMotionAsset . id ,
375
+ livePhotoVideoId : fileStub . livePhotoMotion . uuid ,
364
376
} ) ;
365
377
} ) ;
366
378
367
- it ( 'should create new motion asset if not found and link it with the photo' , async ( ) => {
379
+ it ( 'should extract the EmbeddedVideo tag from Samsung JPEG motion photos' , async ( ) => {
380
+ assetMock . getByIds . mockResolvedValue ( [ { ...assetStub . livePhotoStillAsset , livePhotoVideoId : null } ] ) ;
381
+ metadataMock . readTags . mockResolvedValue ( {
382
+ Directory : 'foo/bar/' ,
383
+ EmbeddedVideoFile : new BinaryField ( 0 , '' ) ,
384
+ EmbeddedVideoType : 'MotionPhoto_Data' ,
385
+ } ) ;
386
+ cryptoRepository . hashSha1 . mockReturnValue ( randomBytes ( 512 ) ) ;
387
+ assetMock . getByChecksum . mockResolvedValue ( null ) ;
388
+ assetMock . create . mockResolvedValue ( assetStub . livePhotoMotionAsset ) ;
389
+ cryptoRepository . randomUUID . mockReturnValue ( fileStub . livePhotoMotion . uuid ) ;
390
+ const video = randomBytes ( 512 ) ;
391
+ metadataMock . extractBinaryTag . mockResolvedValue ( video ) ;
392
+
393
+ await sut . handleMetadataExtraction ( { id : assetStub . livePhotoStillAsset . id } ) ;
394
+ expect ( metadataMock . extractBinaryTag ) . toHaveBeenCalledWith (
395
+ assetStub . livePhotoStillAsset . originalPath ,
396
+ 'EmbeddedVideoFile' ,
397
+ ) ;
398
+ expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . livePhotoStillAsset . id ] ) ;
399
+ expect ( assetMock . create ) . toHaveBeenCalled ( ) ; // This could have arguments added
400
+ expect ( storageMock . writeFile ) . toHaveBeenCalledWith ( assetStub . livePhotoMotionAsset . originalPath , video ) ;
401
+ expect ( assetMock . save ) . toHaveBeenNthCalledWith ( 1 , {
402
+ id : assetStub . livePhotoStillAsset . id ,
403
+ livePhotoVideoId : fileStub . livePhotoMotion . uuid ,
404
+ } ) ;
405
+ } ) ;
406
+
407
+ it ( 'should extract the motion photo video from the XMP directory entry ' , async ( ) => {
368
408
assetMock . getByIds . mockResolvedValue ( [ { ...assetStub . livePhotoStillAsset , livePhotoVideoId : null } ] ) ;
369
409
metadataMock . readTags . mockResolvedValue ( {
370
410
Directory : 'foo/bar/' ,
371
411
MotionPhoto : 1 ,
372
412
MicroVideo : 1 ,
373
413
MicroVideoOffset : 1 ,
374
414
} ) ;
415
+ cryptoRepository . hashSha1 . mockReturnValue ( randomBytes ( 512 ) ) ;
416
+ assetMock . getByChecksum . mockResolvedValue ( null ) ;
417
+ assetMock . create . mockResolvedValue ( assetStub . livePhotoMotionAsset ) ;
418
+ cryptoRepository . randomUUID . mockReturnValue ( fileStub . livePhotoMotion . uuid ) ;
375
419
const video = randomBytes ( 512 ) ;
376
420
storageMock . readFile . mockResolvedValue ( video ) ;
377
- cryptoRepository . hashSha1 . mockReturnValue ( randomBytes ( 512 ) ) ;
378
- assetMock . create . mockResolvedValueOnce ( assetStub . livePhotoMotionAsset ) ;
379
- assetMock . save . mockResolvedValueOnce ( assetStub . livePhotoMotionAsset ) ;
380
421
381
422
await sut . handleMetadataExtraction ( { id : assetStub . livePhotoStillAsset . id } ) ;
382
423
expect ( assetMock . getByIds ) . toHaveBeenCalledWith ( [ assetStub . livePhotoStillAsset . id ] ) ;
383
424
expect ( storageMock . readFile ) . toHaveBeenCalledWith ( assetStub . livePhotoStillAsset . originalPath , expect . any ( Object ) ) ;
384
- expect ( assetMock . create ) . toHaveBeenCalledWith (
385
- expect . objectContaining ( {
386
- type : AssetType . VIDEO ,
387
- originalFileName : assetStub . livePhotoStillAsset . originalFileName ,
388
- isVisible : false ,
389
- isReadOnly : false ,
390
- } ) ,
391
- ) ;
392
- expect ( assetMock . save ) . toHaveBeenCalledWith ( {
425
+ expect ( assetMock . create ) . toHaveBeenCalled ( ) ; // This could have arguments added
426
+ expect ( storageMock . writeFile ) . toHaveBeenCalledWith ( assetStub . livePhotoMotionAsset . originalPath , video ) ;
427
+ expect ( assetMock . save ) . toHaveBeenNthCalledWith ( 1 , {
393
428
id : assetStub . livePhotoStillAsset . id ,
394
- livePhotoVideoId : assetStub . livePhotoMotionAsset . id ,
429
+ livePhotoVideoId : fileStub . livePhotoMotion . uuid ,
395
430
} ) ;
396
- expect ( storageMock . writeFile ) . toHaveBeenCalledWith ( assetStub . livePhotoMotionAsset . originalPath , video ) ;
397
- expect ( jobMock . queue ) . toHaveBeenCalledWith ( {
398
- name : JobName . METADATA_EXTRACTION ,
399
- data : { id : assetStub . livePhotoMotionAsset . id } ,
431
+ } ) ;
432
+
433
+ it ( 'should delete old motion photo video assets if they do not match what is extracted' , async ( ) => {
434
+ assetMock . getByIds . mockResolvedValue ( [ assetStub . livePhotoStillAsset ] ) ;
435
+ metadataMock . readTags . mockResolvedValue ( {
436
+ Directory : 'foo/bar/' ,
437
+ MotionPhoto : 1 ,
438
+ MicroVideo : 1 ,
439
+ MicroVideoOffset : 1 ,
440
+ } ) ;
441
+ cryptoRepository . hashSha1 . mockReturnValue ( randomBytes ( 512 ) ) ;
442
+ assetMock . getByChecksum . mockResolvedValue ( null ) ;
443
+ assetMock . create . mockResolvedValue ( assetStub . livePhotoMotionAsset ) ;
444
+
445
+ await sut . handleMetadataExtraction ( { id : assetStub . livePhotoStillAsset . id } ) ;
446
+ expect ( jobMock . queue ) . toHaveBeenNthCalledWith ( 2 , {
447
+ name : JobName . ASSET_DELETION ,
448
+ data : { id : assetStub . livePhotoStillAsset . livePhotoVideoId } ,
449
+ } ) ;
450
+ } ) ;
451
+
452
+ it ( 'should not create a new motionphoto video asset if the of the extracted video matches an existing asset' , async ( ) => {
453
+ assetMock . getByIds . mockResolvedValue ( [ assetStub . livePhotoStillAsset ] ) ;
454
+ metadataMock . readTags . mockResolvedValue ( {
455
+ Directory : 'foo/bar/' ,
456
+ MotionPhoto : 1 ,
457
+ MicroVideo : 1 ,
458
+ MicroVideoOffset : 1 ,
400
459
} ) ;
460
+ cryptoRepository . hashSha1 . mockReturnValue ( randomBytes ( 512 ) ) ;
461
+ assetMock . getByChecksum . mockResolvedValue ( assetStub . livePhotoMotionAsset ) ;
462
+
463
+ await sut . handleMetadataExtraction ( { id : assetStub . livePhotoStillAsset . id } ) ;
464
+ expect ( assetMock . create ) . toHaveBeenCalledTimes ( 0 ) ;
465
+ expect ( storageMock . writeFile ) . toHaveBeenCalledTimes ( 0 ) ;
466
+ // The still asset gets saved by handleMetadataExtraction, but not the video
467
+ expect ( assetMock . save ) . toHaveBeenCalledTimes ( 1 ) ;
468
+ expect ( jobMock . queue ) . toHaveBeenCalledTimes ( 0 ) ;
401
469
} ) ;
402
470
403
471
it ( 'should save all metadata' , async ( ) => {
0 commit comments