1
0
mirror of https://github.com/immich-app/immich.git synced 2024-11-24 08:52:28 +02:00

fix(server): video duration extraction (#11610)

This commit is contained in:
Michel Heusschen 2024-08-06 18:27:05 +02:00 committed by GitHub
parent f040c9fb38
commit 325fb4b5d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 72 additions and 53 deletions

View File

@ -647,13 +647,19 @@ describe(MetadataService.name, () => {
}); });
}); });
it('should handle duration', async () => { it('should extract duration', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
metadataMock.readTags.mockResolvedValue({ Duration: 6.21 }); mediaMock.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
...probeStub.videoStreamH264.format,
duration: 6.21,
},
});
await sut.handleMetadataExtraction({ id: assetStub.image.id }); await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.upsertExif).toHaveBeenCalled();
expect(assetMock.update).toHaveBeenCalledWith( expect(assetMock.update).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -663,10 +669,15 @@ describe(MetadataService.name, () => {
); );
}); });
it('should handle duration in ISO time string', async () => { it('only extracts duration for videos', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]);
metadataMock.readTags.mockResolvedValue({ Duration: '00:00:08.41' }); mediaMock.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
...probeStub.videoStreamH264.format,
duration: 6.21,
},
});
await sut.handleMetadataExtraction({ id: assetStub.image.id }); await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
@ -674,39 +685,51 @@ describe(MetadataService.name, () => {
expect(assetMock.update).toHaveBeenCalledWith( expect(assetMock.update).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
id: assetStub.image.id, id: assetStub.image.id,
duration: '00:00:08.410', duration: null,
}), }),
); );
}); });
it('should handle duration as an object without Scale', async () => { it('omits duration of zero', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } }); mediaMock.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
...probeStub.videoStreamH264.format,
duration: 0,
},
});
await sut.handleMetadataExtraction({ id: assetStub.image.id }); await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.upsertExif).toHaveBeenCalled();
expect(assetMock.update).toHaveBeenCalledWith( expect(assetMock.update).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
id: assetStub.image.id, id: assetStub.image.id,
duration: '00:00:06.200', duration: null,
}), }),
); );
}); });
it('should handle duration with scale', async () => { it('handles duration of 1 week', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } }); mediaMock.probe.mockResolvedValue({
...probeStub.videoStreamH264,
format: {
...probeStub.videoStreamH264.format,
duration: 604_800,
},
});
await sut.handleMetadataExtraction({ id: assetStub.image.id }); await sut.handleMetadataExtraction({ id: assetStub.video.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]);
expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.upsertExif).toHaveBeenCalled();
expect(assetMock.update).toHaveBeenCalledWith( expect(assetMock.update).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
id: assetStub.image.id, id: assetStub.video.id,
duration: '00:00:06.207', duration: '168:00:00.000',
}), }),
); );
}); });

View File

@ -214,28 +214,7 @@ export class MetadataService implements OnEvents {
const { exifData, tags } = await this.exifData(asset); const { exifData, tags } = await this.exifData(asset);
if (asset.type === AssetType.VIDEO) { if (asset.type === AssetType.VIDEO) {
const { videoStreams } = await this.mediaRepository.probe(asset.originalPath); await this.applyVideoMetadata(asset, exifData);
if (videoStreams[0]) {
switch (videoStreams[0].rotation) {
case -90: {
exifData.orientation = Orientation.Rotate90CW;
break;
}
case 0: {
exifData.orientation = Orientation.Horizontal;
break;
}
case 90: {
exifData.orientation = Orientation.Rotate270CW;
break;
}
case 180: {
exifData.orientation = Orientation.Rotate180;
break;
}
}
}
} }
await this.applyMotionPhotos(asset, tags); await this.applyMotionPhotos(asset, tags);
@ -252,7 +231,7 @@ export class MetadataService implements OnEvents {
} }
await this.assetRepository.update({ await this.assetRepository.update({
id: asset.id, id: asset.id,
duration: tags.Duration ? this.getDuration(tags.Duration) : null, duration: asset.duration,
localDateTime, localDateTime,
fileCreatedAt: exifData.dateTimeOriginal ?? undefined, fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
}); });
@ -567,16 +546,33 @@ export class MetadataService implements OnEvents {
return bitsPerSample; return bitsPerSample;
} }
private getDuration(seconds?: ImmichTags['Duration']): string { private async applyVideoMetadata(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
let _seconds = seconds as number; const { videoStreams, format } = await this.mediaRepository.probe(asset.originalPath);
if (typeof seconds === 'object') { if (videoStreams[0]) {
_seconds = seconds.Value * (seconds?.Scale || 1); switch (videoStreams[0].rotation) {
} else if (typeof seconds === 'string') { case -90: {
_seconds = Duration.fromISOTime(seconds).as('seconds'); exifData.orientation = Orientation.Rotate90CW;
break;
}
case 0: {
exifData.orientation = Orientation.Horizontal;
break;
}
case 90: {
exifData.orientation = Orientation.Rotate270CW;
break;
}
case 180: {
exifData.orientation = Orientation.Rotate180;
break;
}
}
} }
return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS'); if (format.duration) {
asset.duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS');
}
} }
private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> { private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> {