fix(server): only extract image's duration if format supports animation (#24587)
parent
35eda735c8
commit
77926383db
|
|
@ -1034,7 +1034,10 @@ describe(MetadataService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use Duration from exif', async () => {
|
it('should use Duration from exif', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||||
|
...assetStub.image,
|
||||||
|
originalPath: '/original/path.webp',
|
||||||
|
});
|
||||||
mockReadTags({ Duration: 123 }, {});
|
mockReadTags({ Duration: 123 }, {});
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
@ -1046,6 +1049,7 @@ describe(MetadataService.name, () => {
|
||||||
it('should prefer Duration from exif over sidecar', async () => {
|
it('should prefer Duration from exif over sidecar', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||||
...assetStub.image,
|
...assetStub.image,
|
||||||
|
originalPath: '/original/path.webp',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
id: 'some-id',
|
id: 'some-id',
|
||||||
|
|
@ -1063,6 +1067,16 @@ describe(MetadataService.name, () => {
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore all Duration tags for definitely static images', async () => {
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.imageDng);
|
||||||
|
mockReadTags({ Duration: 123 }, { Duration: 456 });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.imageDng.id });
|
||||||
|
|
||||||
|
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null }));
|
||||||
|
});
|
||||||
|
|
||||||
it('should ignore Duration from exif for videos', async () => {
|
it('should ignore Duration from exif for videos', async () => {
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video);
|
||||||
mockReadTags({ Duration: 123 }, {});
|
mockReadTags({ Duration: 123 }, {});
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import { BaseService } from 'src/services/base.service';
|
||||||
import { JobItem, JobOf } from 'src/types';
|
import { JobItem, JobOf } from 'src/types';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { isAssetChecksumConstraint } from 'src/utils/database';
|
import { isAssetChecksumConstraint } from 'src/utils/database';
|
||||||
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { isFaceImportEnabled } from 'src/utils/misc';
|
import { isFaceImportEnabled } from 'src/utils/misc';
|
||||||
import { upsertTags } from 'src/utils/tag';
|
import { upsertTags } from 'src/utils/tag';
|
||||||
|
|
||||||
|
|
@ -486,7 +487,8 @@ export class MetadataService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// prefer duration from video tags
|
// prefer duration from video tags
|
||||||
if (videoTags) {
|
// don't save duration if asset is definitely not an animated image (see e.g. CR3 with Duration: 1s)
|
||||||
|
if (videoTags || !mimeTypes.isPossiblyAnimatedImage(asset.originalPath)) {
|
||||||
delete mediaTags.Duration;
|
delete mediaTags.Duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,26 @@ describe('mimeTypes', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('animated image', () => {
|
||||||
|
for (const img of ['a.avif', 'a.gif', 'a.webp']) {
|
||||||
|
it('should identify animated image mime types as such', () => {
|
||||||
|
expect(mimeTypes.isPossiblyAnimatedImage(img)).toBeTruthy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const img of ['a.cr3', 'a.jpg', 'a.tiff']) {
|
||||||
|
it('should identify static image mime types as such', () => {
|
||||||
|
expect(mimeTypes.isPossiblyAnimatedImage(img)).toBeFalsy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const extension of Object.keys(mimeTypes.video)) {
|
||||||
|
it('should not identify video mime types as animated', () => {
|
||||||
|
expect(mimeTypes.isPossiblyAnimatedImage(extension)).toBeFalsy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe('video', () => {
|
describe('video', () => {
|
||||||
it('should contain only lowercase mime types', () => {
|
it('should contain only lowercase mime types', () => {
|
||||||
const keys = Object.keys(mimeTypes.video);
|
const keys = Object.keys(mimeTypes.video);
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,11 @@ const image: Record<string, string[]> = {
|
||||||
'.tiff': ['image/tiff'],
|
'.tiff': ['image/tiff'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const possiblyAnimatedImageExtensions = new Set(['.avif', '.gif', '.heic', '.heif', '.jxl', '.png', '.webp']);
|
||||||
|
const possiblyAnimatedImage: Record<string, string[]> = Object.fromEntries(
|
||||||
|
Object.entries(image).filter(([key]) => possiblyAnimatedImageExtensions.has(key)),
|
||||||
|
);
|
||||||
|
|
||||||
const extensionOverrides: Record<string, string> = {
|
const extensionOverrides: Record<string, string> = {
|
||||||
'image/jpeg': '.jpg',
|
'image/jpeg': '.jpg',
|
||||||
};
|
};
|
||||||
|
|
@ -119,6 +124,7 @@ export const mimeTypes = {
|
||||||
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
|
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
|
||||||
isImage: (filename: string) => isType(filename, image),
|
isImage: (filename: string) => isType(filename, image),
|
||||||
isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage),
|
isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage),
|
||||||
|
isPossiblyAnimatedImage: (filename: string) => isType(filename, possiblyAnimatedImage),
|
||||||
isProfile: (filename: string) => isType(filename, profile),
|
isProfile: (filename: string) => isType(filename, profile),
|
||||||
isSidecar: (filename: string) => isType(filename, sidecar),
|
isSidecar: (filename: string) => isType(filename, sidecar),
|
||||||
isVideo: (filename: string) => isType(filename, video),
|
isVideo: (filename: string) => isType(filename, video),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue