fix: duration extraction (#24178)
parent
35d18da14a
commit
db15e5e423
|
|
@ -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);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||||
mockReadTags({}, { Duration: { Value: 123 } });
|
mockReadTags({ Duration: 123 }, {});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
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 () => {
|
it('should trim whitespace from description', async () => {
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ export class MetadataService extends BaseService {
|
||||||
this.assetRepository.upsertExif(exifData),
|
this.assetRepository.upsertExif(exifData),
|
||||||
this.assetRepository.update({
|
this.assetRepository.update({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
duration: exifTags.Duration?.toString() ?? null,
|
duration: this.getDuration(exifTags),
|
||||||
localDateTime: dates.localDateTime,
|
localDateTime: dates.localDateTime,
|
||||||
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
|
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
|
||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
|
|
@ -457,19 +457,7 @@ export class MetadataService extends BaseService {
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExifTags(asset: {
|
private async 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: {
|
|
||||||
originalPath: string;
|
originalPath: string;
|
||||||
sidecarPath: string | null;
|
sidecarPath: string | null;
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
|
|
@ -492,7 +480,11 @@ export class MetadataService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefer duration from video tags
|
// prefer duration from video tags
|
||||||
delete mediaTags.Duration;
|
if (videoTags) {
|
||||||
|
delete mediaTags.Duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
// never use duration from sidecar
|
||||||
delete sidecarTags?.Duration;
|
delete sidecarTags?.Duration;
|
||||||
|
|
||||||
return { ...mediaTags, ...videoTags, ...sidecarTags };
|
return { ...mediaTags, ...videoTags, ...sidecarTags };
|
||||||
|
|
@ -934,6 +926,20 @@ export class MetadataService extends BaseService {
|
||||||
return bitsPerSample;
|
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) {
|
private async getVideoTags(originalPath: string) {
|
||||||
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
|
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
|
||||||
|
|
||||||
|
|
@ -961,7 +967,7 @@ export class MetadataService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format.duration) {
|
if (format.duration) {
|
||||||
tags.Duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS');
|
tags.Duration = format.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue