mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -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
					
				@ -1,4 +1,4 @@
 | 
			
		||||
import { BinaryField } from 'exiftool-vendored';
 | 
			
		||||
import { BinaryField, ExifDateTime } from 'exiftool-vendored';
 | 
			
		||||
import { randomBytes } from 'node:crypto';
 | 
			
		||||
import { Stats } from 'node:fs';
 | 
			
		||||
import { constants } from 'node:fs/promises';
 | 
			
		||||
@ -746,6 +746,8 @@ describe(MetadataService.name, () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should save all metadata', async () => {
 | 
			
		||||
      const dateForTest = new Date('1970-01-01T00:00:00.000-11:30');
 | 
			
		||||
 | 
			
		||||
      const tags: ImmichTags = {
 | 
			
		||||
        BitsPerSample: 1,
 | 
			
		||||
        ComponentBitDepth: 1,
 | 
			
		||||
@ -753,7 +755,7 @@ describe(MetadataService.name, () => {
 | 
			
		||||
        BitDepth: 1,
 | 
			
		||||
        ColorBitDepth: 1,
 | 
			
		||||
        ColorSpace: '1',
 | 
			
		||||
        DateTimeOriginal: new Date('1970-01-01').toISOString(),
 | 
			
		||||
        DateTimeOriginal: ExifDateTime.fromISO(dateForTest.toISOString()),
 | 
			
		||||
        ExposureTime: '100ms',
 | 
			
		||||
        FocalLength: 20,
 | 
			
		||||
        ImageDescription: 'test description',
 | 
			
		||||
@ -762,11 +764,11 @@ describe(MetadataService.name, () => {
 | 
			
		||||
        MediaGroupUUID: 'livePhoto',
 | 
			
		||||
        Make: 'test-factory',
 | 
			
		||||
        Model: "'mockel'",
 | 
			
		||||
        ModifyDate: new Date('1970-01-01').toISOString(),
 | 
			
		||||
        ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()),
 | 
			
		||||
        Orientation: 0,
 | 
			
		||||
        ProfileDescription: 'extensive description',
 | 
			
		||||
        ProjectionType: 'equirectangular',
 | 
			
		||||
        tz: '+02:00',
 | 
			
		||||
        tz: 'UTC-11:30',
 | 
			
		||||
        Rating: 3,
 | 
			
		||||
      };
 | 
			
		||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
			
		||||
@ -779,7 +781,7 @@ describe(MetadataService.name, () => {
 | 
			
		||||
        bitsPerSample: expect.any(Number),
 | 
			
		||||
        autoStackId: null,
 | 
			
		||||
        colorspace: tags.ColorSpace,
 | 
			
		||||
        dateTimeOriginal: new Date('1970-01-01'),
 | 
			
		||||
        dateTimeOriginal: dateForTest,
 | 
			
		||||
        description: tags.ImageDescription,
 | 
			
		||||
        exifImageHeight: null,
 | 
			
		||||
        exifImageWidth: null,
 | 
			
		||||
@ -805,11 +807,37 @@ describe(MetadataService.name, () => {
 | 
			
		||||
      expect(assetMock.update).toHaveBeenCalledWith({
 | 
			
		||||
        id: assetStub.image.id,
 | 
			
		||||
        duration: null,
 | 
			
		||||
        fileCreatedAt: new Date('1970-01-01'),
 | 
			
		||||
        localDateTime: new Date('1970-01-01'),
 | 
			
		||||
        fileCreatedAt: dateForTest,
 | 
			
		||||
        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 () => {
 | 
			
		||||
      assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
 | 
			
		||||
      mediaMock.probe.mockResolvedValue({
 | 
			
		||||
 | 
			
		||||
@ -531,12 +531,16 @@ export class MetadataService {
 | 
			
		||||
 | 
			
		||||
    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 = {
 | 
			
		||||
      // altitude: tags.GPSAltitude ?? null,
 | 
			
		||||
      assetId: asset.id,
 | 
			
		||||
      bitsPerSample: this.getBitsPerSample(exifTags),
 | 
			
		||||
      colorspace: exifTags.ColorSpace ?? null,
 | 
			
		||||
      dateTimeOriginal: this.getDateTimeOriginal(exifTags) ?? asset.fileCreatedAt,
 | 
			
		||||
      dateTimeOriginal,
 | 
			
		||||
      description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
 | 
			
		||||
      exifImageHeight: validate(exifTags.ImageHeight),
 | 
			
		||||
      exifImageWidth: validate(exifTags.ImageWidth),
 | 
			
		||||
@ -557,7 +561,7 @@ export class MetadataService {
 | 
			
		||||
      orientation: validate(exifTags.Orientation)?.toString() ?? null,
 | 
			
		||||
      profileDescription: exifTags.ProfileDescription || null,
 | 
			
		||||
      projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
 | 
			
		||||
      timeZone: exifTags.tz ?? null,
 | 
			
		||||
      timeZone,
 | 
			
		||||
      rating: exifTags.Rating ?? null,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -578,10 +582,25 @@ export class MetadataService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
 | 
			
		||||
    if (!tags) {
 | 
			
		||||
      return null;
 | 
			
		||||
    return this.getDateTimeOriginalWithRawValue(tags).exifDate;
 | 
			
		||||
  }
 | 
			
		||||
    return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS));
 | 
			
		||||
 | 
			
		||||
  private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } {
 | 
			
		||||
    if (!tags) {
 | 
			
		||||
      return { exifDate: null, rawValue: '' };
 | 
			
		||||
    }
 | 
			
		||||
    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 {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user