From 29182cfc9a192f0d20e98feff9c9b9f69ffd2874 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 19 Oct 2023 14:51:56 -0400 Subject: [PATCH] fix(server): exif duration with scale (#4541) --- .../domain/metadata/metadata.service.spec.ts | 48 +++++++++++++++++++ .../src/domain/metadata/metadata.service.ts | 9 +++- .../repositories/metadata.repository.ts | 8 +++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index 1096db613..86befcee8 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -409,6 +409,54 @@ describe(MetadataService.name, () => { localDateTime: new Date('1970-01-01'), }); }); + + it('should handle duration', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.getExifTags.mockResolvedValue({ Duration: 6.21 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.upsertExif).toHaveBeenCalled(); + expect(assetMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: assetStub.image.id, + duration: '00:00:06.210', + }), + ); + }); + + it('should handle duration as an object without Scale', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.getExifTags.mockResolvedValue({ Duration: { Value: 6.2 } }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.upsertExif).toHaveBeenCalled(); + expect(assetMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: assetStub.image.id, + duration: '00:00:06.200', + }), + ); + }); + + it('should handle duration with scale', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.getExifTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.upsertExif).toHaveBeenCalled(); + expect(assetMock.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: assetStub.image.id, + duration: '00:00:06.207', + }), + ); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 9a7b4f3e5..0b6855ddf 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -7,6 +7,7 @@ import { Duration } from 'luxon'; import { usePagination } from '../domain.util'; import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; import { + ExifDuration, IAlbumRepository, IAssetRepository, ICryptoRepository, @@ -398,7 +399,11 @@ export class MetadataService { return bitsPerSample; } - private getDuration(seconds?: number): string { - return Duration.fromObject({ seconds }).toFormat('hh:mm:ss.SSS'); + private getDuration(seconds?: number | ExifDuration): string { + let _seconds = seconds as number; + if (typeof seconds === 'object') { + _seconds = seconds.Value * (seconds?.Scale || 1); + } + return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS'); } } diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index 084a655c7..0c3b78462 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -14,7 +14,12 @@ export interface ReverseGeocodeResult { city: string | null; } -export interface ImmichTags extends Omit { +export interface ExifDuration { + Value: number; + Scale?: number; +} + +export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; MotionPhotoVersion?: number; @@ -22,6 +27,7 @@ export interface ImmichTags extends Omit { MediaGroupUUID?: string; ImagePixelDepth?: string; FocalLength?: number; + Duration?: number | ExifDuration; } export interface IMetadataRepository {