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:
parent
ee6550c02c
commit
cbb0a7f8d4
2 changed files with 58 additions and 11 deletions
|
@ -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({
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue