mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	chore(server): unit tests for metadata service (#4280)
* unit tests for metadata service * better test descriptions
This commit is contained in:
		
							parent
							
								
									68d6d89a3b
								
							
						
					
					
						commit
						10d10d9021
					
				@ -148,7 +148,7 @@
 | 
				
			|||||||
    "coverageDirectory": "./coverage",
 | 
					    "coverageDirectory": "./coverage",
 | 
				
			||||||
    "coverageThreshold": {
 | 
					    "coverageThreshold": {
 | 
				
			||||||
      "./src/domain/": {
 | 
					      "./src/domain/": {
 | 
				
			||||||
        "branches": 75,
 | 
					        "branches": 80,
 | 
				
			||||||
        "functions": 80,
 | 
					        "functions": 80,
 | 
				
			||||||
        "lines": 90,
 | 
					        "lines": 90,
 | 
				
			||||||
        "statements": 90
 | 
					        "statements": 90
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import { AssetType, CitiesFile, ExifEntity, SystemConfigKey } from '@app/infra/entities';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  assetStub,
 | 
					  assetStub,
 | 
				
			||||||
  newAlbumRepositoryMock,
 | 
					  newAlbumRepositoryMock,
 | 
				
			||||||
@ -8,14 +9,16 @@ import {
 | 
				
			|||||||
  newStorageRepositoryMock,
 | 
					  newStorageRepositoryMock,
 | 
				
			||||||
  newSystemConfigRepositoryMock,
 | 
					  newSystemConfigRepositoryMock,
 | 
				
			||||||
} from '@test';
 | 
					} from '@test';
 | 
				
			||||||
 | 
					import { randomBytes } from 'crypto';
 | 
				
			||||||
 | 
					import { Stats } from 'fs';
 | 
				
			||||||
import { constants } from 'fs/promises';
 | 
					import { constants } from 'fs/promises';
 | 
				
			||||||
import { IAlbumRepository } from '../album';
 | 
					import { IAlbumRepository } from '../album';
 | 
				
			||||||
import { IAssetRepository, WithProperty, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithProperty, WithoutProperty } from '../asset';
 | 
				
			||||||
import { ICryptoRepository } from '../crypto';
 | 
					import { ICryptoRepository } from '../crypto';
 | 
				
			||||||
import { IJobRepository, JobName } from '../job';
 | 
					import { IJobRepository, JobName, QueueName } from '../job';
 | 
				
			||||||
import { IStorageRepository } from '../storage';
 | 
					import { IStorageRepository } from '../storage';
 | 
				
			||||||
import { ISystemConfigRepository } from '../system-config';
 | 
					import { ISystemConfigRepository } from '../system-config';
 | 
				
			||||||
import { IMetadataRepository } from './metadata.repository';
 | 
					import { IMetadataRepository, ImmichTags } from './metadata.repository';
 | 
				
			||||||
import { MetadataService } from './metadata.service';
 | 
					import { MetadataService } from './metadata.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(MetadataService.name, () => {
 | 
					describe(MetadataService.name, () => {
 | 
				
			||||||
@ -44,6 +47,342 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
    expect(sut).toBeDefined();
 | 
					    expect(sut).toBeDefined();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('init', () => {
 | 
				
			||||||
 | 
					    beforeEach(async () => {
 | 
				
			||||||
 | 
					      configMock.load.mockResolvedValue([
 | 
				
			||||||
 | 
					        { key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true },
 | 
				
			||||||
 | 
					        { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_500 },
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return if reverse geocoding is disabled', async () => {
 | 
				
			||||||
 | 
					      configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					      expect(metadataMock.deleteCache).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.pause).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					      expect(metadataMock.init).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					      expect(jobMock.resume).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should return if deleteCache is false and the cities precision has not changed', async () => {
 | 
				
			||||||
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(metadataMock.deleteCache).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.pause).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					      expect(metadataMock.init).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					      expect(jobMock.resume).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should re-init if deleteCache is false but the cities precision has changed', async () => {
 | 
				
			||||||
 | 
					      configMock.load.mockResolvedValue([
 | 
				
			||||||
 | 
					        { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_1000 },
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(metadataMock.deleteCache).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
 | 
				
			||||||
 | 
					      expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 });
 | 
				
			||||||
 | 
					      expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should re-init and delete cache if deleteCache is true', async () => {
 | 
				
			||||||
 | 
					      await sut.init(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(metadataMock.deleteCache).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
 | 
				
			||||||
 | 
					      expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 });
 | 
				
			||||||
 | 
					      expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleLivePhotoLinking', () => {
 | 
				
			||||||
 | 
					    it('should handle an asset that could not be found', async () => {
 | 
				
			||||||
 | 
					      await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(false);
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(albumMock.removeAsset).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle an asset without exif info', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(false);
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(albumMock.removeAsset).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle livePhotoCID not set', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(true);
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(albumMock.removeAsset).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle not finding a match', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          ...assetStub.livePhotoMotionAsset,
 | 
				
			||||||
 | 
					          exifInfo: { livePhotoCID: assetStub.livePhotoStillAsset.id } as ExifEntity,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(true);
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        livePhotoCID: assetStub.livePhotoStillAsset.id,
 | 
				
			||||||
 | 
					        ownerId: assetStub.livePhotoMotionAsset.ownerId,
 | 
				
			||||||
 | 
					        otherAssetId: assetStub.livePhotoMotionAsset.id,
 | 
				
			||||||
 | 
					        type: AssetType.IMAGE,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(albumMock.removeAsset).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should link photo and video', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          ...assetStub.livePhotoStillAsset,
 | 
				
			||||||
 | 
					          exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					      assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true);
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        livePhotoCID: assetStub.livePhotoMotionAsset.id,
 | 
				
			||||||
 | 
					        ownerId: assetStub.livePhotoStillAsset.ownerId,
 | 
				
			||||||
 | 
					        otherAssetId: assetStub.livePhotoStillAsset.id,
 | 
				
			||||||
 | 
					        type: AssetType.VIDEO,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        id: assetStub.livePhotoStillAsset.id,
 | 
				
			||||||
 | 
					        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
 | 
				
			||||||
 | 
					      expect(albumMock.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleQueueMetadataExtraction', () => {
 | 
				
			||||||
 | 
					    it('should queue metadata extraction for all assets without exif values', async () => {
 | 
				
			||||||
 | 
					      assetMock.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(true);
 | 
				
			||||||
 | 
					      expect(assetMock.getWithout).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        name: JobName.METADATA_EXTRACTION,
 | 
				
			||||||
 | 
					        data: { id: assetStub.image.id },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should queue metadata extraction for all assets', async () => {
 | 
				
			||||||
 | 
					      assetMock.getAll.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(true);
 | 
				
			||||||
 | 
					      expect(assetMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        name: JobName.METADATA_EXTRACTION,
 | 
				
			||||||
 | 
					        data: { id: assetStub.image.id },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('handleMetadataExtraction', () => {
 | 
				
			||||||
 | 
					    beforeEach(() => {
 | 
				
			||||||
 | 
					      storageMock.stat.mockResolvedValue({ size: 123456 } as Stats);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle an asset that could not be found', async () => {
 | 
				
			||||||
 | 
					      await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.upsertExif).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle an asset with isVisible set to false', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, isVisible: false }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(false);
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.upsertExif).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should handle lists of numbers', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					      metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleMetadataExtraction({ id: assetStub.image.id });
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        id: assetStub.image.id,
 | 
				
			||||||
 | 
					        duration: null,
 | 
				
			||||||
 | 
					        fileCreatedAt: assetStub.image.createdAt,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should apply reverse geocoding', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
 | 
				
			||||||
 | 
					      configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
 | 
				
			||||||
 | 
					      metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
 | 
				
			||||||
 | 
					      metadataMock.getExifTags.mockResolvedValue({
 | 
				
			||||||
 | 
					        GPSLatitude: assetStub.withLocation.exifInfo!.latitude!,
 | 
				
			||||||
 | 
					        GPSLongitude: assetStub.withLocation.exifInfo!.longitude!,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleMetadataExtraction({ id: assetStub.image.id });
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.upsertExif).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        id: assetStub.withLocation.id,
 | 
				
			||||||
 | 
					        duration: null,
 | 
				
			||||||
 | 
					        fileCreatedAt: assetStub.withLocation.createdAt,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should not apply motion photos if asset is video', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
 | 
				
			||||||
 | 
					      expect(storageMock.writeFile).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(jobMock.queue).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					      expect(assetMock.save).not.toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should apply motion photos', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
 | 
				
			||||||
 | 
					      metadataMock.getExifTags.mockResolvedValue({
 | 
				
			||||||
 | 
					        Directory: 'foo/bar/',
 | 
				
			||||||
 | 
					        MotionPhoto: 1,
 | 
				
			||||||
 | 
					        MicroVideo: 1,
 | 
				
			||||||
 | 
					        MicroVideoOffset: 1,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      storageMock.readFile.mockResolvedValue(randomBytes(512));
 | 
				
			||||||
 | 
					      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
 | 
				
			||||||
 | 
					      assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
 | 
				
			||||||
 | 
					      expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        id: assetStub.livePhotoStillAsset.id,
 | 
				
			||||||
 | 
					        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should create new motion asset if not found and link it with the photo', async () => {
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
 | 
				
			||||||
 | 
					      metadataMock.getExifTags.mockResolvedValue({
 | 
				
			||||||
 | 
					        Directory: 'foo/bar/',
 | 
				
			||||||
 | 
					        MotionPhoto: 1,
 | 
				
			||||||
 | 
					        MicroVideo: 1,
 | 
				
			||||||
 | 
					        MicroVideoOffset: 1,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      const video = randomBytes(512);
 | 
				
			||||||
 | 
					      storageMock.readFile.mockResolvedValue(video);
 | 
				
			||||||
 | 
					      cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
 | 
				
			||||||
 | 
					      assetMock.save.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
 | 
				
			||||||
 | 
					      expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        id: assetStub.livePhotoStillAsset.id,
 | 
				
			||||||
 | 
					        livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith(
 | 
				
			||||||
 | 
					        expect.objectContaining({
 | 
				
			||||||
 | 
					          type: AssetType.VIDEO,
 | 
				
			||||||
 | 
					          originalFileName: assetStub.livePhotoStillAsset.originalFileName,
 | 
				
			||||||
 | 
					          isVisible: false,
 | 
				
			||||||
 | 
					          isReadOnly: true,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
 | 
				
			||||||
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        name: JobName.METADATA_EXTRACTION,
 | 
				
			||||||
 | 
					        data: { id: assetStub.livePhotoMotionAsset.id },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should save all metadata', async () => {
 | 
				
			||||||
 | 
					      const tags: ImmichTags = {
 | 
				
			||||||
 | 
					        BitsPerSample: 1,
 | 
				
			||||||
 | 
					        ComponentBitDepth: 1,
 | 
				
			||||||
 | 
					        ImagePixelDepth: '1',
 | 
				
			||||||
 | 
					        BitDepth: 1,
 | 
				
			||||||
 | 
					        ColorBitDepth: 1,
 | 
				
			||||||
 | 
					        ColorSpace: '1',
 | 
				
			||||||
 | 
					        DateTimeOriginal: new Date('1970-01-01').toISOString(),
 | 
				
			||||||
 | 
					        ExposureTime: '100ms',
 | 
				
			||||||
 | 
					        FocalLength: 20,
 | 
				
			||||||
 | 
					        ISO: 100,
 | 
				
			||||||
 | 
					        LensModel: 'test lens',
 | 
				
			||||||
 | 
					        MediaGroupUUID: 'livePhoto',
 | 
				
			||||||
 | 
					        Make: 'test-factory',
 | 
				
			||||||
 | 
					        Model: "'mockel'",
 | 
				
			||||||
 | 
					        ModifyDate: new Date('1970-01-01').toISOString(),
 | 
				
			||||||
 | 
					        Orientation: 0,
 | 
				
			||||||
 | 
					        ProfileDescription: 'extensive description',
 | 
				
			||||||
 | 
					        ProjectionType: 'equirectangular',
 | 
				
			||||||
 | 
					        tz: '+02:00',
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					      metadataMock.getExifTags.mockResolvedValue(tags);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await sut.handleMetadataExtraction({ id: assetStub.image.id });
 | 
				
			||||||
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
 | 
				
			||||||
 | 
					      expect(assetMock.upsertExif).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        assetId: assetStub.image.id,
 | 
				
			||||||
 | 
					        bitsPerSample: expect.any(Number),
 | 
				
			||||||
 | 
					        colorspace: tags.ColorSpace,
 | 
				
			||||||
 | 
					        dateTimeOriginal: new Date('1970-01-01'),
 | 
				
			||||||
 | 
					        exifImageHeight: null,
 | 
				
			||||||
 | 
					        exifImageWidth: null,
 | 
				
			||||||
 | 
					        exposureTime: tags.ExposureTime,
 | 
				
			||||||
 | 
					        fNumber: null,
 | 
				
			||||||
 | 
					        fileSizeInByte: 123456,
 | 
				
			||||||
 | 
					        focalLength: tags.FocalLength,
 | 
				
			||||||
 | 
					        fps: null,
 | 
				
			||||||
 | 
					        iso: tags.ISO,
 | 
				
			||||||
 | 
					        latitude: null,
 | 
				
			||||||
 | 
					        lensModel: tags.LensModel,
 | 
				
			||||||
 | 
					        livePhotoCID: tags.MediaGroupUUID,
 | 
				
			||||||
 | 
					        longitude: null,
 | 
				
			||||||
 | 
					        make: tags.Make,
 | 
				
			||||||
 | 
					        model: tags.Model,
 | 
				
			||||||
 | 
					        modifyDate: expect.any(Date),
 | 
				
			||||||
 | 
					        orientation: tags.Orientation?.toString(),
 | 
				
			||||||
 | 
					        profileDescription: tags.ProfileDescription,
 | 
				
			||||||
 | 
					        projectionType: 'EQUIRECTANGULAR',
 | 
				
			||||||
 | 
					        timeZone: tags.tz,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
 | 
					        id: assetStub.image.id,
 | 
				
			||||||
 | 
					        duration: null,
 | 
				
			||||||
 | 
					        fileCreatedAt: new Date('1970-01-01'),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('handleQueueSidecar', () => {
 | 
					  describe('handleQueueSidecar', () => {
 | 
				
			||||||
    it('should queue assets with sidecar files', async () => {
 | 
					    it('should queue assets with sidecar files', async () => {
 | 
				
			||||||
      assetMock.getWith.mockResolvedValue({ items: [assetStub.sidecar], hasNextPage: false });
 | 
					      assetMock.getWith.mockResolvedValue({ items: [assetStub.sidecar], hasNextPage: false });
 | 
				
			||||||
@ -122,14 +461,4 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe('handleMetadataExtraction', () => {
 | 
					 | 
				
			||||||
    it('should handle lists of numbers', async () => {
 | 
					 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image1]);
 | 
					 | 
				
			||||||
      storageMock.stat.mockResolvedValue({ size: 123456 } as any);
 | 
					 | 
				
			||||||
      metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any });
 | 
					 | 
				
			||||||
      await sut.handleMetadataExtraction({ id: assetStub.image1.id });
 | 
					 | 
				
			||||||
      expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -264,7 +264,7 @@ export class MetadataService {
 | 
				
			|||||||
        position,
 | 
					        position,
 | 
				
			||||||
        length,
 | 
					        length,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const checksum = await this.cryptoRepository.hashSha1(video);
 | 
					      const checksum = this.cryptoRepository.hashSha1(video);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
 | 
					      let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
 | 
				
			||||||
      if (!motionAsset) {
 | 
					      if (!motionAsset) {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user