|
|
|
|
@ -3,7 +3,6 @@ import { randomBytes } from 'node:crypto';
|
|
|
|
|
import { Stats } from 'node:fs';
|
|
|
|
|
import { constants } from 'node:fs/promises';
|
|
|
|
|
import { defaults } from 'src/config';
|
|
|
|
|
import { Exif } from 'src/database';
|
|
|
|
|
import { AssetEntity } from 'src/entities/asset.entity';
|
|
|
|
|
import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
|
|
|
|
|
import { WithoutProperty } from 'src/repositories/asset.repository';
|
|
|
|
|
@ -144,9 +143,10 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle an asset that could not be found', async () => {
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(void 0);
|
|
|
|
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).not.toHaveBeenCalled();
|
|
|
|
|
expect(mocks.asset.update).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
@ -154,11 +154,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
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');
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.sidecar);
|
|
|
|
|
mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.sidecar.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
@ -173,7 +173,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
it('should take the file modification date when missing exif and earlier than creation date', async () => {
|
|
|
|
|
const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z');
|
|
|
|
|
const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z');
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
size: 123_456,
|
|
|
|
|
mtime: fileModifiedAt,
|
|
|
|
|
@ -183,7 +183,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
mockReadTags();
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ dateTimeOriginal: fileModifiedAt }),
|
|
|
|
|
);
|
|
|
|
|
@ -199,7 +199,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
it('should take the file creation date when missing exif and earlier than modification date', async () => {
|
|
|
|
|
const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z');
|
|
|
|
|
const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z');
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
size: 123_456,
|
|
|
|
|
mtime: fileModifiedAt,
|
|
|
|
|
@ -209,7 +209,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
mockReadTags();
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: fileCreatedAt }));
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
|
|
|
|
id: assetStub.image.id,
|
|
|
|
|
@ -222,7 +222,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
|
|
|
|
|
it('should account for the server being in a non-UTC timezone', async () => {
|
|
|
|
|
process.env.TZ = 'America/Los_Angeles';
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.sidecar]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.sidecar);
|
|
|
|
|
mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -240,7 +240,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle lists of numbers', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
size: 123_456,
|
|
|
|
|
mtime: assetStub.image.fileModifiedAt,
|
|
|
|
|
@ -252,7 +252,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }));
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith({
|
|
|
|
|
id: assetStub.image.id,
|
|
|
|
|
@ -265,7 +265,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
|
|
|
|
|
it('should not delete latituide and longitude without reverse geocode', async () => {
|
|
|
|
|
// regression test for issue 17511
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } });
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
size: 123_456,
|
|
|
|
|
@ -279,7 +279,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ city: null, state: null, country: null }),
|
|
|
|
|
);
|
|
|
|
|
@ -293,7 +293,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should apply reverse geocoding', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } });
|
|
|
|
|
mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' });
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
@ -308,7 +308,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }),
|
|
|
|
|
);
|
|
|
|
|
@ -322,19 +322,19 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should discard latitude and longitude on null island', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.withLocation]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation);
|
|
|
|
|
mockReadTags({
|
|
|
|
|
GPSLatitude: 0,
|
|
|
|
|
GPSLongitude: 0,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract tags from TagsList', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ TagsList: ['Parent'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -344,7 +344,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract hierarchy from TagsList', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ TagsList: ['Parent/Child'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
|
|
|
|
@ -364,7 +364,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract tags from Keywords as a string', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Keywords: 'Parent' });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -374,7 +374,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract tags from Keywords as a list', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Keywords: ['Parent'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -384,7 +384,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract tags from Keywords as a list with a number', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Keywords: ['Parent', 2024] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -395,7 +395,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract hierarchal tags from Keywords', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Keywords: 'Parent/Child' });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -414,7 +414,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore Keywords when TagsList is present', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -433,7 +433,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract hierarchy from HierarchicalSubject', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert);
|
|
|
|
|
@ -454,7 +454,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract tags from HierarchicalSubject as a list with a number', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ HierarchicalSubject: ['Parent', 2024] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -465,7 +465,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ HierarchicalSubject: ['Mom/Dad'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -479,7 +479,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
|
|
|
|
mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert);
|
|
|
|
|
|
|
|
|
|
@ -498,7 +498,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should remove existing tags', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({});
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -507,13 +507,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not apply motion photos if asset is video', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
|
|
|
|
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], {
|
|
|
|
|
faces: { person: false },
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
|
|
|
|
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
|
|
|
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
|
|
|
|
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
|
|
|
|
@ -523,7 +521,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle an invalid Directory Item', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({
|
|
|
|
|
MotionPhoto: 1,
|
|
|
|
|
ContainerDirectory: [{ Foo: 100 }],
|
|
|
|
|
@ -533,19 +531,24 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract the correct video orientation', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
|
|
|
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
|
|
|
|
mockReadTags({});
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => {
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoWithOriginalFileName,
|
|
|
|
|
livePhotoVideoId: null,
|
|
|
|
|
libraryId: null,
|
|
|
|
|
});
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
size: 123_456,
|
|
|
|
|
@ -573,9 +576,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
|
|
|
|
'MotionPhotoVideo',
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
|
|
|
|
|
faces: { person: false },
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id);
|
|
|
|
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
|
|
|
|
checksum: expect.any(Buffer),
|
|
|
|
|
deviceAssetId: 'NONE',
|
|
|
|
|
@ -607,7 +608,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(),
|
|
|
|
|
birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(),
|
|
|
|
|
} as Stats);
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoWithOriginalFileName,
|
|
|
|
|
livePhotoVideoId: null,
|
|
|
|
|
libraryId: null,
|
|
|
|
|
});
|
|
|
|
|
mockReadTags({
|
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
|
EmbeddedVideoFile: new BinaryField(0, ''),
|
|
|
|
|
@ -625,9 +630,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
|
|
|
|
'EmbeddedVideoFile',
|
|
|
|
|
);
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
|
|
|
|
|
faces: { person: false },
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id);
|
|
|
|
|
expect(mocks.asset.create).toHaveBeenCalledWith({
|
|
|
|
|
checksum: expect.any(Buffer),
|
|
|
|
|
deviceAssetId: 'NONE',
|
|
|
|
|
@ -653,7 +656,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract the motion photo video from the XMP directory entry ', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoWithOriginalFileName, livePhotoVideoId: null }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoWithOriginalFileName,
|
|
|
|
|
livePhotoVideoId: null,
|
|
|
|
|
libraryId: null,
|
|
|
|
|
});
|
|
|
|
|
mocks.storage.stat.mockResolvedValue({
|
|
|
|
|
size: 123_456,
|
|
|
|
|
mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
|
|
|
|
|
@ -673,9 +680,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
mocks.storage.readFile.mockResolvedValue(video);
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoWithOriginalFileName.id], {
|
|
|
|
|
faces: { person: false },
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id);
|
|
|
|
|
expect(mocks.storage.readFile).toHaveBeenCalledWith(
|
|
|
|
|
assetStub.livePhotoWithOriginalFileName.originalPath,
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
@ -705,7 +710,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should delete old motion photo video assets if they do not match what is extracted', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoWithOriginalFileName]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoWithOriginalFileName);
|
|
|
|
|
mockReadTags({
|
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
|
MotionPhoto: 1,
|
|
|
|
|
@ -727,7 +732,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset);
|
|
|
|
|
mockReadTags({
|
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
|
MotionPhoto: 1,
|
|
|
|
|
@ -749,7 +754,10 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
|
livePhotoVideoId: null,
|
|
|
|
|
});
|
|
|
|
|
mockReadTags({
|
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
|
MotionPhoto: 1,
|
|
|
|
|
@ -774,9 +782,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not update storage usage if motion photo is external', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([
|
|
|
|
|
{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null, isExternal: true },
|
|
|
|
|
]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
|
livePhotoVideoId: null,
|
|
|
|
|
isExternal: true,
|
|
|
|
|
});
|
|
|
|
|
mockReadTags({
|
|
|
|
|
Directory: 'foo/bar/',
|
|
|
|
|
MotionPhoto: 1,
|
|
|
|
|
@ -818,11 +828,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
tz: 'UTC-11:30',
|
|
|
|
|
Rating: 3,
|
|
|
|
|
};
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags(tags);
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith({
|
|
|
|
|
assetId: assetStub.image.id,
|
|
|
|
|
bitsPerSample: expect.any(Number),
|
|
|
|
|
@ -878,11 +888,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
|
|
|
|
tz: undefined,
|
|
|
|
|
};
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags(tags);
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
timeZone: 'UTC+0',
|
|
|
|
|
@ -891,7 +901,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should extract duration', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
|
|
|
|
mocks.media.probe.mockResolvedValue({
|
|
|
|
|
...probeStub.videoStreamH264,
|
|
|
|
|
format: {
|
|
|
|
|
@ -902,7 +912,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
@ -913,7 +923,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should only extract duration for videos', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mocks.media.probe.mockResolvedValue({
|
|
|
|
|
...probeStub.videoStreamH264,
|
|
|
|
|
format: {
|
|
|
|
|
@ -923,7 +933,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
@ -934,7 +944,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should omit duration of zero', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
|
|
|
|
mocks.media.probe.mockResolvedValue({
|
|
|
|
|
...probeStub.videoStreamH264,
|
|
|
|
|
format: {
|
|
|
|
|
@ -945,7 +955,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
@ -956,7 +966,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should a handle duration of 1 week', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
|
|
|
|
mocks.media.probe.mockResolvedValue({
|
|
|
|
|
...probeStub.videoStreamH264,
|
|
|
|
|
format: {
|
|
|
|
|
@ -967,7 +977,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.video.id });
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.video.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id);
|
|
|
|
|
expect(mocks.asset.upsertExif).toHaveBeenCalled();
|
|
|
|
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
@ -978,7 +988,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore duration from exif data', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({}, { Duration: { Value: 123 } });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -986,7 +996,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should trim whitespace from description', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Description: '\t \v \f \n \r' });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1006,7 +1016,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle a numeric description', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Description: 1000 });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1018,7 +1028,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should skip importing metadata when the feature is disabled', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
|
|
|
|
mockReadTags(makeFaceTags({ Name: 'Person 1' }));
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1026,7 +1036,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
|
|
|
|
mockReadTags();
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1034,7 +1044,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should skip importing faces without name', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
|
|
|
|
mockReadTags(makeFaceTags());
|
|
|
|
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
|
|
|
|
@ -1046,7 +1056,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should skip importing faces with empty name', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
|
|
|
|
mockReadTags(makeFaceTags({ Name: '' }));
|
|
|
|
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
|
|
|
|
@ -1058,14 +1068,14 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should apply metadata face tags creating new persons', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
|
|
|
|
mockReadTags(makeFaceTags({ Name: personStub.withName.name }));
|
|
|
|
|
mocks.person.getDistinctNames.mockResolvedValue([]);
|
|
|
|
|
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
|
|
|
|
mocks.person.update.mockResolvedValue(personStub.withName);
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id);
|
|
|
|
|
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
|
|
|
|
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
|
|
|
|
expect.objectContaining({ name: personStub.withName.name }),
|
|
|
|
|
@ -1099,14 +1109,14 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should assign metadata face tags to existing persons', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage);
|
|
|
|
|
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
|
|
|
|
mockReadTags(makeFaceTags({ Name: personStub.withName.name }));
|
|
|
|
|
mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
|
|
|
|
mocks.person.createAll.mockResolvedValue([]);
|
|
|
|
|
mocks.person.update.mockResolvedValue(personStub.withName);
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id);
|
|
|
|
|
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
|
|
|
|
expect(mocks.person.createAll).not.toHaveBeenCalled();
|
|
|
|
|
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
|
|
|
|
@ -1131,7 +1141,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle invalid modify date', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ ModifyDate: '00:00:00.000' });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1143,7 +1153,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle invalid rating value', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Rating: 6 });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1155,7 +1165,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle valid rating value', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Rating: 5 });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1167,7 +1177,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle valid negative rating value', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags({ Rating: -1 });
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1179,11 +1189,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle livePhotoCID not set', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
|
|
|
|
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
|
|
|
|
|
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
|
|
|
|
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
|
|
|
|
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
|
|
|
|
@ -1191,20 +1201,19 @@ describe(MetadataService.name, () => {
|
|
|
|
|
|
|
|
|
|
it('should handle not finding a match', async () => {
|
|
|
|
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
mockReadTags({ ContentIdentifier: 'CID' });
|
|
|
|
|
|
|
|
|
|
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
|
|
|
|
JobStatus.SUCCESS,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], {
|
|
|
|
|
faces: { person: false },
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
|
|
|
|
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
|
|
|
|
livePhotoCID: 'CID',
|
|
|
|
|
ownerId: assetStub.livePhotoMotionAsset.ownerId,
|
|
|
|
|
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
|
|
|
|
libraryId: null,
|
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
|
|
|
|
@ -1212,7 +1221,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should link photo and video', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset);
|
|
|
|
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
mockReadTags({ ContentIdentifier: 'CID' });
|
|
|
|
|
|
|
|
|
|
@ -1220,9 +1229,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
JobStatus.SUCCESS,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], {
|
|
|
|
|
faces: { person: false },
|
|
|
|
|
});
|
|
|
|
|
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.id);
|
|
|
|
|
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
|
|
|
|
livePhotoCID: 'CID',
|
|
|
|
|
ownerId: assetStub.livePhotoStillAsset.ownerId,
|
|
|
|
|
@ -1238,12 +1245,9 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should notify clients on live photo link', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([
|
|
|
|
|
{
|
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
|
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as Exif,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
|
});
|
|
|
|
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
mockReadTags({ ContentIdentifier: 'CID' });
|
|
|
|
|
|
|
|
|
|
@ -1258,12 +1262,11 @@ describe(MetadataService.name, () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should search by libraryId', async () => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([
|
|
|
|
|
{
|
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
|
libraryId: 'library-id',
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
|
|
|
|
...assetStub.livePhotoStillAsset,
|
|
|
|
|
libraryId: 'library-id',
|
|
|
|
|
});
|
|
|
|
|
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
|
|
|
|
mockReadTags({ ContentIdentifier: 'CID' });
|
|
|
|
|
|
|
|
|
|
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
|
|
|
|
@ -1296,7 +1299,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
},
|
|
|
|
|
{ exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } },
|
|
|
|
|
])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags(exif);
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
@ -1318,7 +1321,7 @@ describe(MetadataService.name, () => {
|
|
|
|
|
{ exif: { LensID: ' Unknown 6-30mm' }, expected: null },
|
|
|
|
|
{ exif: { LensID: '' }, expected: null },
|
|
|
|
|
])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => {
|
|
|
|
|
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
|
|
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
|
|
|
|
mockReadTags(exif);
|
|
|
|
|
|
|
|
|
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
|
|
|
|
|