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:
parent
f040c9fb38
commit
325fb4b5d1
@ -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',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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> {
|
||||||
|
Loading…
Reference in New Issue
Block a user