Skip to content

Commit db15e5e

Browse files
authored
fix: duration extraction (#24178)
1 parent 35d18da commit db15e5e

File tree

2 files changed

+57
-19
lines changed

2 files changed

+57
-19
lines changed

server/src/services/metadata.service.spec.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,12 +1017,44 @@ describe(MetadataService.name, () => {
10171017
);
10181018
});
10191019

1020-
it('should ignore duration from exif data', async () => {
1020+
it('should use Duration from exif', async () => {
10211021
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
1022-
mockReadTags({}, { Duration: { Value: 123 } });
1022+
mockReadTags({ Duration: 123 }, {});
10231023

10241024
await sut.handleMetadataExtraction({ id: assetStub.image.id });
1025-
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null }));
1025+
1026+
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
1027+
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
1028+
});
1029+
1030+
it('should prefer Duration from exif over sidecar', async () => {
1031+
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
1032+
...assetStub.image,
1033+
sidecarPath: '/path/to/something',
1034+
});
1035+
mockReadTags({ Duration: 123 }, { Duration: 456 });
1036+
1037+
await sut.handleMetadataExtraction({ id: assetStub.image.id });
1038+
1039+
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2);
1040+
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
1041+
});
1042+
1043+
it('should ignore Duration from exif for videos', async () => {
1044+
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
1045+
mockReadTags({ Duration: 123 }, {});
1046+
mocks.media.probe.mockResolvedValue({
1047+
...probeStub.videoStreamH264,
1048+
format: {
1049+
...probeStub.videoStreamH264.format,
1050+
duration: 456,
1051+
},
1052+
});
1053+
1054+
await sut.handleMetadataExtraction({ id: assetStub.video.id });
1055+
1056+
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
1057+
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' }));
10261058
});
10271059

10281060
it('should trim whitespace from description', async () => {

server/src/services/metadata.service.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ export class MetadataService extends BaseService {
291291
this.assetRepository.upsertExif(exifData),
292292
this.assetRepository.update({
293293
id: asset.id,
294-
duration: exifTags.Duration?.toString() ?? null,
294+
duration: this.getDuration(exifTags),
295295
localDateTime: dates.localDateTime,
296296
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
297297
fileModifiedAt: stats.mtime,
@@ -457,19 +457,7 @@ export class MetadataService extends BaseService {
457457
return { width, height };
458458
}
459459

460-
private getExifTags(asset: {
461-
originalPath: string;
462-
sidecarPath: string | null;
463-
type: AssetType;
464-
}): Promise<ImmichTags> {
465-
if (!asset.sidecarPath && asset.type === AssetType.Image) {
466-
return this.metadataRepository.readTags(asset.originalPath);
467-
}
468-
469-
return this.mergeExifTags(asset);
470-
}
471-
472-
private async mergeExifTags(asset: {
460+
private async getExifTags(asset: {
473461
originalPath: string;
474462
sidecarPath: string | null;
475463
type: AssetType;
@@ -492,7 +480,11 @@ export class MetadataService extends BaseService {
492480
}
493481

494482
// prefer duration from video tags
495-
delete mediaTags.Duration;
483+
if (videoTags) {
484+
delete mediaTags.Duration;
485+
}
486+
487+
// never use duration from sidecar
496488
delete sidecarTags?.Duration;
497489

498490
return { ...mediaTags, ...videoTags, ...sidecarTags };
@@ -934,6 +926,20 @@ export class MetadataService extends BaseService {
934926
return bitsPerSample;
935927
}
936928

929+
private getDuration(tags: ImmichTags): string | null {
930+
const duration = tags.Duration;
931+
932+
if (typeof duration === 'string') {
933+
return duration;
934+
}
935+
936+
if (typeof duration === 'number') {
937+
return Duration.fromObject({ seconds: duration }).toFormat('hh:mm:ss.SSS');
938+
}
939+
940+
return null;
941+
}
942+
937943
private async getVideoTags(originalPath: string) {
938944
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
939945

@@ -961,7 +967,7 @@ export class MetadataService extends BaseService {
961967
}
962968

963969
if (format.duration) {
964-
tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS');
970+
tags.Duration = format.duration;
965971
}
966972

967973
return tags;

0 commit comments

Comments
 (0)