fix(server): override date via xmp (#5199)

* Fix

* open api

* Change to list and delete

* Bug fix

* Change name

* refactor: clean up code and add test

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
YFrendo 2023-11-21 17:58:56 +01:00 committed by GitHub
parent 55fa3234fd
commit 0f657da5a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 54 additions and 13 deletions

View File

@ -14,6 +14,7 @@ import {
import { randomBytes } from 'crypto';
import { Stats } from 'fs';
import { constants } from 'fs/promises';
import { when } from 'jest-when';
import { JobName, QueueName } from '../job';
import {
IAlbumRepository,
@ -248,6 +249,30 @@ describe(MetadataService.name, () => {
expect(assetMock.save).not.toHaveBeenCalled();
});
it('should handle a date in a sidecar file', async () => {
const originalDate = new Date('2023-11-21T16:13:17.517Z');
const sidecarDate = new Date('2022-01-01T00:00:00.000Z');
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
when(metadataMock.getExifTags)
.calledWith(assetStub.sidecar.originalPath)
// higher priority tag
.mockResolvedValue({ CreationDate: originalDate.toISOString() });
when(metadataMock.getExifTags)
.calledWith(assetStub.sidecar.sidecarPath as string)
// lower priority tag, but in sidecar
.mockResolvedValue({ CreateDate: sidecarDate.toISOString() });
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]);
expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }));
expect(assetMock.save).toHaveBeenCalledWith({
id: assetStub.image.id,
duration: null,
fileCreatedAt: sidecarDate,
localDateTime: sidecarDate,
});
});
it('should handle lists of numbers', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any });

View File

@ -25,6 +25,18 @@ import {
import { StorageCore } from '../storage';
import { FeatureFlag, SystemConfigCore } from '../system-config';
/** look for a date from these tags (in order) */
const EXIF_DATE_TAGS: Array<keyof Tags> = [
'SubSecDateTimeOriginal',
'DateTimeOriginal',
'SubSecCreateDate',
'CreationDate',
'CreateDate',
'SubSecMediaCreateDate',
'MediaCreateDate',
'DateTimeCreated',
];
interface DirectoryItem {
Length?: number;
Mime: string;
@ -340,6 +352,15 @@ export class MetadataService {
const stats = await this.storageRepository.stat(asset.originalPath);
const mediaTags = await this.repository.getExifTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null;
// ensure date from sidecar is used if present
const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags);
if (mediaTags && hasDateOverride) {
for (const tag of EXIF_DATE_TAGS) {
delete mediaTags[tag];
}
}
const tags = { ...mediaTags, ...sidecarTags };
this.logger.verbose('Exif Tags', tags);
@ -350,19 +371,7 @@ export class MetadataService {
assetId: asset.id,
bitsPerSample: this.getBitsPerSample(tags),
colorspace: tags.ColorSpace ?? null,
dateTimeOriginal:
exifDate(
firstDateTime(tags as Tags, [
'SubSecDateTimeOriginal',
'DateTimeOriginal',
'SubSecCreateDate',
'CreationDate',
'CreateDate',
'SubSecMediaCreateDate',
'MediaCreateDate',
'DateTimeCreated',
]),
) ?? asset.fileCreatedAt,
dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt,
exifImageHeight: validate(tags.ImageHeight),
exifImageWidth: validate(tags.ImageWidth),
exposureTime: tags.ExposureTime ?? null,
@ -387,6 +396,13 @@ export class MetadataService {
};
}
private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
if (!tags) {
return null;
}
return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS));
}
private getBitsPerSample(tags: ImmichTags): number | null {
const bitDepthTags = [
tags.BitsPerSample,