0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-28 00:59:18 -05:00

fix(server): parse time zone with explicit zero offset (#12307)

* fix(server): fix test: use data as returned by exiftool-vendored

* fix(server): retain +00:00 timezone if set explicitly
This commit is contained in:
Carsten Otto 2024-09-04 16:27:04 +02:00 committed by GitHub
parent ee6550c02c
commit cbb0a7f8d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import { BinaryField } from 'exiftool-vendored'; import { BinaryField, ExifDateTime } from 'exiftool-vendored';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { Stats } from 'node:fs'; import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises'; import { constants } from 'node:fs/promises';
@ -746,6 +746,8 @@ describe(MetadataService.name, () => {
}); });
it('should save all metadata', async () => { it('should save all metadata', async () => {
const dateForTest = new Date('1970-01-01T00:00:00.000-11:30');
const tags: ImmichTags = { const tags: ImmichTags = {
BitsPerSample: 1, BitsPerSample: 1,
ComponentBitDepth: 1, ComponentBitDepth: 1,
@ -753,7 +755,7 @@ describe(MetadataService.name, () => {
BitDepth: 1, BitDepth: 1,
ColorBitDepth: 1, ColorBitDepth: 1,
ColorSpace: '1', ColorSpace: '1',
DateTimeOriginal: new Date('1970-01-01').toISOString(), DateTimeOriginal: ExifDateTime.fromISO(dateForTest.toISOString()),
ExposureTime: '100ms', ExposureTime: '100ms',
FocalLength: 20, FocalLength: 20,
ImageDescription: 'test description', ImageDescription: 'test description',
@ -762,11 +764,11 @@ describe(MetadataService.name, () => {
MediaGroupUUID: 'livePhoto', MediaGroupUUID: 'livePhoto',
Make: 'test-factory', Make: 'test-factory',
Model: "'mockel'", Model: "'mockel'",
ModifyDate: new Date('1970-01-01').toISOString(), ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()),
Orientation: 0, Orientation: 0,
ProfileDescription: 'extensive description', ProfileDescription: 'extensive description',
ProjectionType: 'equirectangular', ProjectionType: 'equirectangular',
tz: '+02:00', tz: 'UTC-11:30',
Rating: 3, Rating: 3,
}; };
assetMock.getByIds.mockResolvedValue([assetStub.image]); assetMock.getByIds.mockResolvedValue([assetStub.image]);
@ -779,7 +781,7 @@ describe(MetadataService.name, () => {
bitsPerSample: expect.any(Number), bitsPerSample: expect.any(Number),
autoStackId: null, autoStackId: null,
colorspace: tags.ColorSpace, colorspace: tags.ColorSpace,
dateTimeOriginal: new Date('1970-01-01'), dateTimeOriginal: dateForTest,
description: tags.ImageDescription, description: tags.ImageDescription,
exifImageHeight: null, exifImageHeight: null,
exifImageWidth: null, exifImageWidth: null,
@ -805,11 +807,37 @@ describe(MetadataService.name, () => {
expect(assetMock.update).toHaveBeenCalledWith({ expect(assetMock.update).toHaveBeenCalledWith({
id: assetStub.image.id, id: assetStub.image.id,
duration: null, duration: null,
fileCreatedAt: new Date('1970-01-01'), fileCreatedAt: dateForTest,
localDateTime: new Date('1970-01-01'), localDateTime: dateForTest,
}); });
}); });
it('should extract +00:00 timezone from raw value', async () => {
// exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly
// https://github.com/photostructure/exiftool-vendored.js/issues/203
// this only tests our assumptions of exiftool-vendored, demonstrating the issue
const someDate = '2024-09-01T00:00:00.000';
expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC');
expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0
expect(ExifDateTime.fromISO(someDate + '+04:00')?.zone).toBe('UTC+4');
const tags: ImmichTags = {
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
tz: undefined,
};
assetMock.getByIds.mockResolvedValue([assetStub.image]);
metadataMock.readTags.mockResolvedValue(tags);
await sut.handleMetadataExtraction({ id: assetStub.image.id });
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
expect(assetMock.upsertExif).toHaveBeenCalledWith(
expect.objectContaining({
timeZone: 'UTC+0',
}),
);
});
it('should extract duration', async () => { it('should extract duration', async () => {
assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
mediaMock.probe.mockResolvedValue({ mediaMock.probe.mockResolvedValue({

View file

@ -531,12 +531,16 @@ export class MetadataService {
this.logger.verbose('Exif Tags', exifTags); this.logger.verbose('Exif Tags', exifTags);
const dateTimeOriginalWithRawValue = this.getDateTimeOriginalWithRawValue(exifTags);
const dateTimeOriginal = dateTimeOriginalWithRawValue.exifDate ?? asset.fileCreatedAt;
const timeZone = this.getTimeZone(exifTags, dateTimeOriginalWithRawValue.rawValue);
const exifData = { const exifData = {
// altitude: tags.GPSAltitude ?? null, // altitude: tags.GPSAltitude ?? null,
assetId: asset.id, assetId: asset.id,
bitsPerSample: this.getBitsPerSample(exifTags), bitsPerSample: this.getBitsPerSample(exifTags),
colorspace: exifTags.ColorSpace ?? null, colorspace: exifTags.ColorSpace ?? null,
dateTimeOriginal: this.getDateTimeOriginal(exifTags) ?? asset.fileCreatedAt, dateTimeOriginal,
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
exifImageHeight: validate(exifTags.ImageHeight), exifImageHeight: validate(exifTags.ImageHeight),
exifImageWidth: validate(exifTags.ImageWidth), exifImageWidth: validate(exifTags.ImageWidth),
@ -557,7 +561,7 @@ export class MetadataService {
orientation: validate(exifTags.Orientation)?.toString() ?? null, orientation: validate(exifTags.Orientation)?.toString() ?? null,
profileDescription: exifTags.ProfileDescription || null, profileDescription: exifTags.ProfileDescription || null,
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
timeZone: exifTags.tz ?? null, timeZone,
rating: exifTags.Rating ?? null, rating: exifTags.Rating ?? null,
}; };
@ -578,10 +582,25 @@ export class MetadataService {
} }
private getDateTimeOriginal(tags: ImmichTags | Tags | null) { private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
return this.getDateTimeOriginalWithRawValue(tags).exifDate;
}
private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } {
if (!tags) { if (!tags) {
return null; return { exifDate: null, rawValue: '' };
} }
return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS)); const first = firstDateTime(tags as Tags, EXIF_DATE_TAGS);
return { exifDate: exifDate(first), rawValue: first?.rawValue ?? '' };
}
private getTimeZone(exifTags: ImmichTags, rawValue: string) {
const timeZone = exifTags.tz ?? null;
if (timeZone == null && rawValue.endsWith('+00:00')) {
// exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly
// https://github.com/photostructure/exiftool-vendored.js/issues/203
return 'UTC+0';
}
return timeZone;
} }
private getBitsPerSample(tags: ImmichTags): number | null { private getBitsPerSample(tags: ImmichTags): number | null {