2024-01-22 19:04:45 +01:00
|
|
|
import { BinaryField } from 'exiftool-vendored';
|
2023-11-21 17:58:56 +01:00
|
|
|
import { when } from 'jest-when';
|
2024-02-02 04:18:00 +01:00
|
|
|
import { randomBytes } from 'node:crypto';
|
|
|
|
import { Stats } from 'node:fs';
|
|
|
|
import { constants } from 'node:fs/promises';
|
2024-03-20 22:02:51 +01:00
|
|
|
import { AssetType } from 'src/entities/asset.entity';
|
|
|
|
import { ExifEntity } from 'src/entities/exif.entity';
|
|
|
|
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|
|
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
|
|
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
|
|
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
2024-03-22 23:24:02 +01:00
|
|
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
|
|
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
|
|
|
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
|
|
|
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
|
|
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
|
|
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|
|
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
2024-03-21 00:07:30 +01:00
|
|
|
import { MetadataService, Orientation } from 'src/services/metadata.service';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { assetStub } from 'test/fixtures/asset.stub';
|
|
|
|
import { fileStub } from 'test/fixtures/file.stub';
|
|
|
|
import { probeStub } from 'test/fixtures/media.stub';
|
|
|
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
|
|
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
|
|
|
import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock';
|
|
|
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
2024-03-22 23:24:02 +01:00
|
|
|
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|
|
|
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
|
|
|
|
import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock';
|
|
|
|
import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock';
|
|
|
|
import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock';
|
|
|
|
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
|
|
|
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
2023-05-26 14:52:52 +02:00
|
|
|
|
|
|
|
describe(MetadataService.name, () => {
|
2023-09-27 20:44:51 +02:00
|
|
|
let albumMock: jest.Mocked<IAlbumRepository>;
|
2023-05-26 14:52:52 +02:00
|
|
|
let assetMock: jest.Mocked<IAssetRepository>;
|
2023-09-27 20:44:51 +02:00
|
|
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
|
|
|
let cryptoRepository: jest.Mocked<ICryptoRepository>;
|
2023-05-26 14:52:52 +02:00
|
|
|
let jobMock: jest.Mocked<IJobRepository>;
|
2023-09-27 20:44:51 +02:00
|
|
|
let metadataMock: jest.Mocked<IMetadataRepository>;
|
2023-10-11 04:14:44 +02:00
|
|
|
let moveMock: jest.Mocked<IMoveRepository>;
|
2023-12-03 23:34:23 +01:00
|
|
|
let mediaMock: jest.Mocked<IMediaRepository>;
|
2023-10-11 04:14:44 +02:00
|
|
|
let personMock: jest.Mocked<IPersonRepository>;
|
2023-05-26 14:52:52 +02:00
|
|
|
let storageMock: jest.Mocked<IStorageRepository>;
|
2024-03-22 23:24:02 +01:00
|
|
|
let eventMock: jest.Mocked<IEventRepository>;
|
2023-12-28 00:36:51 +01:00
|
|
|
let databaseMock: jest.Mocked<IDatabaseRepository>;
|
2023-09-27 20:44:51 +02:00
|
|
|
let sut: MetadataService;
|
2023-05-26 14:52:52 +02:00
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
beforeEach(() => {
|
2023-09-27 20:44:51 +02:00
|
|
|
albumMock = newAlbumRepositoryMock();
|
2023-05-26 14:52:52 +02:00
|
|
|
assetMock = newAssetRepositoryMock();
|
2023-09-27 20:44:51 +02:00
|
|
|
configMock = newSystemConfigRepositoryMock();
|
|
|
|
cryptoRepository = newCryptoRepositoryMock();
|
2023-05-26 14:52:52 +02:00
|
|
|
jobMock = newJobRepositoryMock();
|
2023-09-27 20:44:51 +02:00
|
|
|
metadataMock = newMetadataRepositoryMock();
|
2023-10-11 04:14:44 +02:00
|
|
|
moveMock = newMoveRepositoryMock();
|
|
|
|
personMock = newPersonRepositoryMock();
|
2024-03-22 23:24:02 +01:00
|
|
|
eventMock = newEventRepositoryMock();
|
2023-05-26 14:52:52 +02:00
|
|
|
storageMock = newStorageRepositoryMock();
|
2023-12-03 23:34:23 +01:00
|
|
|
mediaMock = newMediaRepositoryMock();
|
2023-12-28 00:36:51 +01:00
|
|
|
databaseMock = newDatabaseRepositoryMock();
|
2023-05-26 14:52:52 +02:00
|
|
|
|
2023-10-11 04:14:44 +02:00
|
|
|
sut = new MetadataService(
|
|
|
|
albumMock,
|
|
|
|
assetMock,
|
2024-03-22 23:24:02 +01:00
|
|
|
eventMock,
|
2023-10-11 04:14:44 +02:00
|
|
|
cryptoRepository,
|
2023-12-28 00:36:51 +01:00
|
|
|
databaseMock,
|
2023-10-11 04:14:44 +02:00
|
|
|
jobMock,
|
2023-12-03 23:34:23 +01:00
|
|
|
mediaMock,
|
2023-12-28 00:36:51 +01:00
|
|
|
metadataMock,
|
2023-10-11 04:14:44 +02:00
|
|
|
moveMock,
|
|
|
|
personMock,
|
2023-12-28 00:36:51 +01:00
|
|
|
storageMock,
|
|
|
|
configMock,
|
2023-10-11 04:14:44 +02:00
|
|
|
);
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|
|
|
|
|
2023-11-01 04:08:21 +01:00
|
|
|
afterEach(async () => {
|
|
|
|
await sut.teardown();
|
|
|
|
});
|
|
|
|
|
2023-05-26 14:52:52 +02:00
|
|
|
it('should be defined', () => {
|
|
|
|
expect(sut).toBeDefined();
|
|
|
|
});
|
|
|
|
|
2023-09-29 23:25:45 +02:00
|
|
|
describe('init', () => {
|
|
|
|
beforeEach(async () => {
|
2023-11-25 19:53:30 +01:00
|
|
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]);
|
2023-09-29 23:25:45 +02:00
|
|
|
|
|
|
|
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(jobMock.pause).toHaveBeenCalledTimes(1);
|
|
|
|
expect(metadataMock.init).toHaveBeenCalledTimes(1);
|
|
|
|
expect(jobMock.resume).toHaveBeenCalledTimes(1);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleLivePhotoLinking', () => {
|
|
|
|
it('should handle an asset that could not be found', async () => {
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
2024-03-14 06:58:09 +01:00
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle an asset without exif info', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
2024-03-14 06:58:09 +01:00
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(albumMock.removeAsset).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle livePhotoCID not set', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
2024-03-14 06:58:09 +01:00
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-09-29 23:25:45 +02:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
|
|
|
JobStatus.SKIPPED,
|
|
|
|
);
|
2024-03-14 06:58:09 +01:00
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
|
|
|
|
livePhotoCID: assetStub.livePhotoStillAsset.id,
|
|
|
|
ownerId: assetStub.livePhotoMotionAsset.ownerId,
|
|
|
|
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
});
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-09-29 23:25:45 +02:00
|
|
|
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);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
|
|
|
JobStatus.SUCCESS,
|
|
|
|
);
|
2024-03-14 06:58:09 +01:00
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
|
|
|
|
livePhotoCID: assetStub.livePhotoMotionAsset.id,
|
|
|
|
ownerId: assetStub.livePhotoStillAsset.ownerId,
|
|
|
|
otherAssetId: assetStub.livePhotoStillAsset.id,
|
|
|
|
type: AssetType.VIDEO,
|
|
|
|
});
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-09-29 23:25:45 +02:00
|
|
|
id: assetStub.livePhotoStillAsset.id,
|
|
|
|
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
|
|
|
});
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(albumMock.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
|
|
|
});
|
2023-12-06 15:56:09 +01:00
|
|
|
|
|
|
|
it('should notify clients on live photo link', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([
|
|
|
|
{
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
|
|
|
},
|
|
|
|
]);
|
|
|
|
assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
|
|
|
JobStatus.SUCCESS,
|
|
|
|
);
|
2024-03-22 23:24:02 +01:00
|
|
|
expect(eventMock.clientSend).toHaveBeenCalledWith(
|
2023-12-13 18:23:51 +01:00
|
|
|
ClientEvent.ASSET_HIDDEN,
|
2023-12-06 15:56:09 +01:00
|
|
|
assetStub.livePhotoMotionAsset.ownerId,
|
|
|
|
assetStub.livePhotoMotionAsset.id,
|
|
|
|
);
|
|
|
|
});
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleQueueMetadataExtraction', () => {
|
|
|
|
it('should queue metadata extraction for all assets without exif values', async () => {
|
|
|
|
assetMock.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.getWithout).toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: { id: assetStub.image.id },
|
|
|
|
},
|
|
|
|
]);
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should queue metadata extraction for all assets', async () => {
|
|
|
|
assetMock.getAll.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.SUCCESS);
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.getAll).toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
|
|
|
data: { id: assetStub.image.id },
|
|
|
|
},
|
|
|
|
]);
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleMetadataExtraction', () => {
|
|
|
|
beforeEach(() => {
|
2024-02-02 04:18:00 +01:00
|
|
|
storageMock.stat.mockResolvedValue({ size: 123_456 } as Stats);
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle an asset that could not be found', async () => {
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
2023-09-29 23:25:45 +02:00
|
|
|
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).not.toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
|
2023-11-21 17:58:56 +01:00
|
|
|
it('should handle a date in a sidecar file', async () => {
|
|
|
|
const originalDate = new Date('2023-11-21T16:13:17.517Z');
|
|
|
|
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
2023-11-30 04:52:28 +01:00
|
|
|
when(metadataMock.readTags)
|
2023-11-21 17:58:56 +01:00
|
|
|
.calledWith(assetStub.sidecar.originalPath)
|
|
|
|
// higher priority tag
|
|
|
|
.mockResolvedValue({ CreationDate: originalDate.toISOString() });
|
2023-11-30 04:52:28 +01:00
|
|
|
when(metadataMock.readTags)
|
2023-11-21 17:58:56 +01:00
|
|
|
.calledWith(assetStub.sidecar.sidecarPath as string)
|
|
|
|
// lower priority tag, but in sidecar
|
|
|
|
.mockResolvedValue({ CreateDate: sidecarDate.toISOString() });
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-11-21 17:58:56 +01:00
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: null,
|
|
|
|
fileCreatedAt: sidecarDate,
|
|
|
|
localDateTime: sidecarDate,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-09-29 23:25:45 +02:00
|
|
|
it('should handle lists of numbers', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({ ISO: [160] as any });
|
2023-09-29 23:25:45 +02:00
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-09-29 23:25:45 +02:00
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: null,
|
|
|
|
fileCreatedAt: assetStub.image.createdAt,
|
2023-10-05 00:11:11 +02:00
|
|
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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' });
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({
|
2023-09-29 23:25:45 +02:00
|
|
|
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' }),
|
|
|
|
);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-09-29 23:25:45 +02:00
|
|
|
id: assetStub.withLocation.id,
|
|
|
|
duration: null,
|
|
|
|
fileCreatedAt: assetStub.withLocation.createdAt,
|
2023-10-31 11:08:34 +01:00
|
|
|
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-12-11 16:00:23 +01:00
|
|
|
it('should discard latitude and longitude on null island', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.withLocation]);
|
|
|
|
metadataMock.readTags.mockResolvedValue({
|
|
|
|
GPSLatitude: 0,
|
|
|
|
GPSLongitude: 0,
|
|
|
|
});
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null }));
|
|
|
|
});
|
|
|
|
|
2023-09-29 23:25:45 +02:00
|
|
|
it('should not apply motion photos if asset is video', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
2023-12-03 23:34:23 +01:00
|
|
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
2023-09-29 23:25:45 +02:00
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]);
|
|
|
|
expect(storageMock.writeFile).not.toHaveBeenCalled();
|
|
|
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalledWith(
|
2023-09-29 23:25:45 +02:00
|
|
|
expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2023-12-03 23:34:23 +01:00
|
|
|
it('should extract the correct video orientation', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
|
|
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
|
|
|
metadataMock.readTags.mockResolvedValue(null);
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
|
|
|
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
|
|
|
expect.objectContaining({ orientation: Orientation.Rotate270CW }),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-01-22 19:04:45 +01:00
|
|
|
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
2023-09-29 23:25:45 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({
|
2023-09-29 23:25:45 +02:00
|
|
|
Directory: 'foo/bar/',
|
2024-01-22 19:04:45 +01:00
|
|
|
MotionPhotoVideo: new BinaryField(0, ''),
|
|
|
|
// The below two are included to ensure that the MotionPhotoVideo tag is extracted
|
|
|
|
// instead of the EmbeddedVideoFile, since HEIC MotionPhotos include both
|
|
|
|
EmbeddedVideoFile: new BinaryField(0, ''),
|
|
|
|
EmbeddedVideoType: 'MotionPhoto_Data',
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
2024-01-22 19:04:45 +01:00
|
|
|
assetMock.getByChecksum.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
|
|
|
|
const video = randomBytes(512);
|
|
|
|
metadataMock.extractBinaryTag.mockResolvedValue(video);
|
2023-09-29 23:25:45 +02:00
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
2024-01-22 19:04:45 +01:00
|
|
|
expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith(
|
|
|
|
assetStub.livePhotoStillAsset.originalPath,
|
|
|
|
'MotionPhotoVideo',
|
|
|
|
);
|
2023-09-29 23:25:45 +02:00
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
2024-01-22 19:04:45 +01:00
|
|
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
|
|
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
2023-09-29 23:25:45 +02:00
|
|
|
id: assetStub.livePhotoStillAsset.id,
|
2024-01-22 19:04:45 +01:00
|
|
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
|
|
|
metadataMock.readTags.mockResolvedValue({
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
EmbeddedVideoFile: new BinaryField(0, ''),
|
|
|
|
EmbeddedVideoType: 'MotionPhoto_Data',
|
|
|
|
});
|
|
|
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
|
|
|
assetMock.getByChecksum.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
|
|
|
|
const video = randomBytes(512);
|
|
|
|
metadataMock.extractBinaryTag.mockResolvedValue(video);
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
|
|
|
expect(metadataMock.extractBinaryTag).toHaveBeenCalledWith(
|
|
|
|
assetStub.livePhotoStillAsset.originalPath,
|
|
|
|
'EmbeddedVideoFile',
|
|
|
|
);
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
|
|
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
|
|
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
2024-01-22 19:04:45 +01:00
|
|
|
id: assetStub.livePhotoStillAsset.id,
|
|
|
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-01-22 19:04:45 +01:00
|
|
|
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
2023-09-29 23:25:45 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({
|
2023-09-29 23:25:45 +02:00
|
|
|
Directory: 'foo/bar/',
|
|
|
|
MotionPhoto: 1,
|
|
|
|
MicroVideo: 1,
|
|
|
|
MicroVideoOffset: 1,
|
|
|
|
});
|
2024-01-22 19:04:45 +01:00
|
|
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
|
|
|
assetMock.getByChecksum.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
cryptoRepository.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
|
2023-09-29 23:25:45 +02:00
|
|
|
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));
|
2024-01-22 19:04:45 +01:00
|
|
|
expect(assetMock.create).toHaveBeenCalled(); // This could have arguments added
|
|
|
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenNthCalledWith(1, {
|
2023-10-05 00:11:11 +02:00
|
|
|
id: assetStub.livePhotoStillAsset.id,
|
2024-01-22 19:04:45 +01:00
|
|
|
livePhotoVideoId: fileStub.livePhotoMotion.uuid,
|
2023-10-05 00:11:11 +02:00
|
|
|
});
|
2024-01-22 19:04:45 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
|
|
|
metadataMock.readTags.mockResolvedValue({
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
MotionPhoto: 1,
|
|
|
|
MicroVideo: 1,
|
|
|
|
MicroVideoOffset: 1,
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
2024-01-22 19:04:45 +01:00
|
|
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
|
|
|
assetMock.getByChecksum.mockResolvedValue(null);
|
|
|
|
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
|
|
|
expect(jobMock.queue).toHaveBeenNthCalledWith(2, {
|
|
|
|
name: JobName.ASSET_DELETION,
|
|
|
|
data: { id: assetStub.livePhotoStillAsset.livePhotoVideoId },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not create a new motionphoto video asset if the of the extracted video matches an existing asset', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
|
|
|
metadataMock.readTags.mockResolvedValue({
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
MotionPhoto: 1,
|
|
|
|
MicroVideo: 1,
|
|
|
|
MicroVideoOffset: 1,
|
|
|
|
});
|
|
|
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
|
|
|
assetMock.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
|
|
|
expect(assetMock.create).toHaveBeenCalledTimes(0);
|
|
|
|
expect(storageMock.writeFile).toHaveBeenCalledTimes(0);
|
|
|
|
// The still asset gets saved by handleMetadataExtraction, but not the video
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledTimes(1);
|
2024-01-22 19:04:45 +01:00
|
|
|
expect(jobMock.queue).toHaveBeenCalledTimes(0);
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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,
|
2024-01-15 17:19:41 +01:00
|
|
|
ImageDescription: 'test description',
|
2023-09-29 23:25:45 +02:00
|
|
|
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]);
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue(tags);
|
2023-09-29 23:25:45 +02:00
|
|
|
|
|
|
|
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),
|
2024-01-27 19:52:14 +01:00
|
|
|
autoStackId: null,
|
2023-09-29 23:25:45 +02:00
|
|
|
colorspace: tags.ColorSpace,
|
|
|
|
dateTimeOriginal: new Date('1970-01-01'),
|
2024-01-15 17:19:41 +01:00
|
|
|
description: tags.ImageDescription,
|
2023-09-29 23:25:45 +02:00
|
|
|
exifImageHeight: null,
|
|
|
|
exifImageWidth: null,
|
|
|
|
exposureTime: tags.ExposureTime,
|
|
|
|
fNumber: null,
|
2024-02-02 04:18:00 +01:00
|
|
|
fileSizeInByte: 123_456,
|
2023-09-29 23:25:45 +02:00
|
|
|
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,
|
|
|
|
});
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-09-29 23:25:45 +02:00
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: null,
|
|
|
|
fileCreatedAt: new Date('1970-01-01'),
|
2023-10-05 00:11:11 +02:00
|
|
|
localDateTime: new Date('1970-01-01'),
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
});
|
2023-10-19 20:51:56 +02:00
|
|
|
|
|
|
|
it('should handle duration', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({ Duration: 6.21 });
|
2023-10-19 20:51:56 +02:00
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith(
|
2023-10-19 20:51:56 +02:00
|
|
|
expect.objectContaining({
|
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: '00:00:06.210',
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-02-02 21:58:13 +01:00
|
|
|
it('should handle duration in ISO time string', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
metadataMock.readTags.mockResolvedValue({ Duration: '00:00:08.41' });
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith(
|
2024-02-02 21:58:13 +01:00
|
|
|
expect.objectContaining({
|
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: '00:00:08.410',
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2023-10-19 20:51:56 +02:00
|
|
|
it('should handle duration as an object without Scale', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2023-11-30 04:52:28 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } });
|
2023-10-19 20:51:56 +02:00
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith(
|
2023-10-19 20:51:56 +02:00
|
|
|
expect.objectContaining({
|
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: '00:00:06.200',
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle duration with scale', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2024-02-02 04:18:00 +01:00
|
|
|
metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } });
|
2023-10-19 20:51:56 +02:00
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
|
|
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
|
|
|
expect(assetMock.upsertExif).toHaveBeenCalled();
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith(
|
2023-10-19 20:51:56 +02:00
|
|
|
expect.objectContaining({
|
|
|
|
id: assetStub.image.id,
|
|
|
|
duration: '00:00:06.207',
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
2023-09-29 23:25:45 +02:00
|
|
|
});
|
|
|
|
|
2023-05-26 14:52:52 +02:00
|
|
|
describe('handleQueueSidecar', () => {
|
|
|
|
it('should queue assets with sidecar files', async () => {
|
2024-02-07 18:30:38 +01:00
|
|
|
assetMock.getAll.mockResolvedValue({ items: [assetStub.sidecar], hasNextPage: false });
|
2023-05-26 14:52:52 +02:00
|
|
|
|
|
|
|
await sut.handleQueueSidecar({ force: true });
|
|
|
|
|
2024-02-07 18:30:38 +01:00
|
|
|
expect(assetMock.getAll).toHaveBeenCalledWith({ take: 1000, skip: 0 });
|
2023-05-26 14:52:52 +02:00
|
|
|
expect(assetMock.getWithout).not.toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.SIDECAR_SYNC,
|
|
|
|
data: { id: assetStub.sidecar.id },
|
|
|
|
},
|
|
|
|
]);
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should queue assets without sidecar files', async () => {
|
2023-08-01 03:28:07 +02:00
|
|
|
assetMock.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
2023-05-26 14:52:52 +02:00
|
|
|
|
|
|
|
await sut.handleQueueSidecar({ force: false });
|
|
|
|
|
|
|
|
expect(assetMock.getWithout).toHaveBeenCalledWith({ take: 1000, skip: 0 }, WithoutProperty.SIDECAR);
|
2024-02-07 18:30:38 +01:00
|
|
|
expect(assetMock.getAll).not.toHaveBeenCalled();
|
2024-01-01 21:45:42 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
|
|
|
{
|
|
|
|
name: JobName.SIDECAR_DISCOVERY,
|
|
|
|
data: { id: assetStub.image.id },
|
|
|
|
},
|
|
|
|
]);
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleSidecarSync', () => {
|
2024-02-07 18:30:38 +01:00
|
|
|
it('should do nothing if asset could not be found', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([]);
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2024-02-07 18:30:38 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should do nothing if asset has no sidecar path', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2024-02-07 18:30:38 +01:00
|
|
|
});
|
|
|
|
|
2024-03-13 18:14:26 +01:00
|
|
|
it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => {
|
2024-02-07 18:30:38 +01:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
2024-02-07 18:30:38 +01:00
|
|
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2024-03-13 18:14:26 +01:00
|
|
|
id: assetStub.sidecar.id,
|
|
|
|
sidecarPath: assetStub.sidecar.sidecarPath,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should set sidecar path if exists (sidecar named photo.xmp)', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt]);
|
|
|
|
storageMock.checkFileExists.mockResolvedValueOnce(false);
|
|
|
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(JobStatus.SUCCESS);
|
2024-03-13 18:14:26 +01:00
|
|
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
|
|
|
2,
|
|
|
|
assetStub.sidecarWithoutExt.sidecarPath,
|
|
|
|
constants.R_OK,
|
|
|
|
);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2024-03-13 18:14:26 +01:00
|
|
|
id: assetStub.sidecarWithoutExt.id,
|
|
|
|
sidecarPath: assetStub.sidecarWithoutExt.sidecarPath,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should set sidecar path if exists (two sidecars named photo.ext.xmp and photo.xmp, should pick photo.ext.xmp)', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
|
|
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
2024-03-13 18:14:26 +01:00
|
|
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK);
|
|
|
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
|
|
|
2,
|
|
|
|
assetStub.sidecarWithoutExt.sidecarPath,
|
|
|
|
constants.R_OK,
|
|
|
|
);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2024-02-07 18:30:38 +01:00
|
|
|
id: assetStub.sidecar.id,
|
|
|
|
sidecarPath: assetStub.sidecar.sidecarPath,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should unset sidecar path if file does not exist anymore', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
storageMock.checkFileExists.mockResolvedValue(false);
|
|
|
|
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
2024-02-07 18:30:38 +01:00
|
|
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2024-02-07 18:30:38 +01:00
|
|
|
id: assetStub.sidecar.id,
|
|
|
|
sidecarPath: null,
|
|
|
|
});
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleSidecarDiscovery', () => {
|
|
|
|
it('should skip hidden assets', async () => {
|
2023-08-01 03:28:07 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
|
|
|
await sut.handleSidecarDiscovery({ id: assetStub.livePhotoMotionAsset.id });
|
2023-05-26 14:52:52 +02:00
|
|
|
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should skip assets with a sidecar path', async () => {
|
2023-08-01 03:28:07 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
await sut.handleSidecarDiscovery({ id: assetStub.sidecar.id });
|
2023-05-26 14:52:52 +02:00
|
|
|
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should do nothing when a sidecar is not found ', async () => {
|
2023-08-01 03:28:07 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2023-05-26 14:52:52 +02:00
|
|
|
storageMock.checkFileExists.mockResolvedValue(false);
|
2023-08-01 03:28:07 +02:00
|
|
|
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).not.toHaveBeenCalled();
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should update a image asset when a sidecar is found', async () => {
|
2023-08-01 03:28:07 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2023-05-26 14:52:52 +02:00
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
2023-08-01 03:28:07 +02:00
|
|
|
await sut.handleSidecarDiscovery({ id: assetStub.image.id });
|
2023-07-10 19:56:45 +02:00
|
|
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-08-01 03:28:07 +02:00
|
|
|
id: assetStub.image.id,
|
2023-07-10 19:56:45 +02:00
|
|
|
sidecarPath: '/original/path.jpg.xmp',
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should update a video asset when a sidecar is found', async () => {
|
2023-08-01 03:28:07 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
2023-05-26 14:52:52 +02:00
|
|
|
storageMock.checkFileExists.mockResolvedValue(true);
|
2023-08-01 03:28:07 +02:00
|
|
|
await sut.handleSidecarDiscovery({ id: assetStub.video.id });
|
2023-06-30 18:25:08 +02:00
|
|
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK);
|
2024-03-20 03:42:10 +01:00
|
|
|
expect(assetMock.update).toHaveBeenCalledWith({
|
2023-08-01 03:28:07 +02:00
|
|
|
id: assetStub.image.id,
|
2023-05-26 14:52:52 +02:00
|
|
|
sidecarPath: '/original/path.ext.xmp',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-11-30 04:52:28 +01:00
|
|
|
|
|
|
|
describe('handleSidecarWrite', () => {
|
|
|
|
it('should skip assets that do not exist anymore', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([]);
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED);
|
2023-11-30 04:52:28 +01:00
|
|
|
expect(metadataMock.writeTags).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should skip jobs with not metadata', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
2024-03-15 14:16:54 +01:00
|
|
|
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SKIPPED);
|
2023-11-30 04:52:28 +01:00
|
|
|
expect(metadataMock.writeTags).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should write tags', async () => {
|
|
|
|
const description = 'this is a description';
|
|
|
|
const gps = 12;
|
|
|
|
const date = '2023-11-22T04:56:12.196Z';
|
|
|
|
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
await expect(
|
|
|
|
sut.handleSidecarWrite({
|
|
|
|
id: assetStub.sidecar.id,
|
|
|
|
description,
|
|
|
|
latitude: gps,
|
|
|
|
longitude: gps,
|
|
|
|
dateTimeOriginal: date,
|
|
|
|
}),
|
2024-03-15 14:16:54 +01:00
|
|
|
).resolves.toBe(JobStatus.SUCCESS);
|
2023-11-30 04:52:28 +01:00
|
|
|
expect(metadataMock.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, {
|
|
|
|
ImageDescription: description,
|
|
|
|
CreationDate: date,
|
|
|
|
GPSLatitude: gps,
|
|
|
|
GPSLongitude: gps,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2023-05-26 14:52:52 +02:00
|
|
|
});
|