fix: duration extraction (#24178)

This commit is contained in:
Jason Rasmussen
2025-11-25 10:26:25 -05:00
committed by GitHub
parent 35d18da14a
commit db15e5e423
2 changed files with 57 additions and 19 deletions

View File

@@ -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 () => {

View File

@@ -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<ImmichTags> {
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
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;