diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 220216a2c8..1760c8a3d7 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1017,12 +1017,44 @@ describe(MetadataService.name, () => { ); }); - it('should ignore duration from exif data', async () => { + it('should use Duration from exif', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); - mockReadTags({}, { Duration: { Value: 123 } }); + mockReadTags({ Duration: 123 }, {}); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null })); + + expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); + expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' })); + }); + + it('should prefer Duration from exif over sidecar', async () => { + mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ + ...assetStub.image, + sidecarPath: '/path/to/something', + }); + mockReadTags({ Duration: 123 }, { Duration: 456 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2); + expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' })); + }); + + it('should ignore Duration from exif for videos', async () => { + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + mockReadTags({ Duration: 123 }, {}); + mocks.media.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 456, + }, + }); + + await sut.handleMetadataExtraction({ id: assetStub.video.id }); + + expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); + expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' })); }); it('should trim whitespace from description', async () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 746f62a944..9f5ce7654c 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -291,7 +291,7 @@ export class MetadataService extends BaseService { this.assetRepository.upsertExif(exifData), this.assetRepository.update({ id: asset.id, - duration: exifTags.Duration?.toString() ?? null, + duration: this.getDuration(exifTags), localDateTime: dates.localDateTime, fileCreatedAt: dates.dateTimeOriginal ?? undefined, fileModifiedAt: stats.mtime, @@ -457,19 +457,7 @@ export class MetadataService extends BaseService { return { width, height }; } - private getExifTags(asset: { - originalPath: string; - sidecarPath: string | null; - type: AssetType; - }): Promise { - if (!asset.sidecarPath && asset.type === AssetType.Image) { - return this.metadataRepository.readTags(asset.originalPath); - } - - return this.mergeExifTags(asset); - } - - private async mergeExifTags(asset: { + private async getExifTags(asset: { originalPath: string; sidecarPath: string | null; type: AssetType; @@ -492,7 +480,11 @@ export class MetadataService extends BaseService { } // prefer duration from video tags - delete mediaTags.Duration; + if (videoTags) { + delete mediaTags.Duration; + } + + // never use duration from sidecar delete sidecarTags?.Duration; return { ...mediaTags, ...videoTags, ...sidecarTags }; @@ -934,6 +926,20 @@ export class MetadataService extends BaseService { return bitsPerSample; } + private getDuration(tags: ImmichTags): string | null { + const duration = tags.Duration; + + if (typeof duration === 'string') { + return duration; + } + + if (typeof duration === 'number') { + return Duration.fromObject({ seconds: duration }).toFormat('hh:mm:ss.SSS'); + } + + return null; + } + private async getVideoTags(originalPath: string) { const { videoStreams, format } = await this.mediaRepository.probe(originalPath); @@ -961,7 +967,7 @@ export class MetadataService extends BaseService { } if (format.duration) { - tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS'); + tags.Duration = format.duration; } return tags;