From 3bb9c17881ef45ed684fdc71cb11157a00edfaa0 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 26 Sep 2024 10:52:21 -0400 Subject: [PATCH] fix(server): handle numeric hierarchical subject values --- server/src/interfaces/metadata.interface.ts | 21 +++++++++++++++++--- server/src/services/metadata.service.spec.ts | 15 ++++++++++++-- server/src/services/metadata.service.ts | 15 +++++++------- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts index 1805969beb046..574420e27a1c8 100644 --- a/server/src/interfaces/metadata.interface.ts +++ b/server/src/interfaces/metadata.interface.ts @@ -7,7 +7,18 @@ export interface ExifDuration { Scale?: number; } -type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo'; +type StringOrNumber = string | number; + +type TagsWithWrongTypes = + | 'FocalLength' + | 'Duration' + | 'Description' + | 'ImageDescription' + | 'RegionInfo' + | 'TagsList' + | 'Keywords' + | 'HierarchicalSubject' + | 'ISO'; export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; @@ -20,10 +31,14 @@ export interface ImmichTags extends Omit { EmbeddedVideoType?: string; EmbeddedVideoFile?: BinaryField; MotionPhotoVideo?: BinaryField; + TagsList?: StringOrNumber[]; + HierarchicalSubject?: StringOrNumber[]; + Keywords?: StringOrNumber | StringOrNumber[]; + ISO?: number | number[]; // Type is wrong, can also be number. - Description?: string | number; - ImageDescription?: string | number; + Description?: StringOrNumber; + ImageDescription?: StringOrNumber; // Extended properties for image regions, such as faces RegionInfo?: { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index ad01aa5784afe..c74883c283bb2 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -316,7 +316,7 @@ describe(MetadataService.name, () => { it('should handle lists of numbers', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ ISO: [160] as any }); + metadataMock.readTags.mockResolvedValue({ ISO: [160] }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); @@ -411,7 +411,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list with a number', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] }); + metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] }); tagMock.upsertValue.mockResolvedValue(tagStub.parent); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -467,6 +467,17 @@ describe(MetadataService.name, () => { expect(tagMock.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined }); }); + it('should extract tags from HierarchicalSubject as a list with a number', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent', 2024] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined }); + }); + it('should extract ignore / characters in a HierarchicalSubject tag', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index bf76be07311b2..224ef03b3b019 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -11,6 +11,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetType, SourceType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; @@ -236,7 +237,7 @@ export class MetadataService { const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags); const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding); - const exifData = { + const exifData: Partial = { assetId: asset.id, // dates @@ -264,7 +265,7 @@ export class MetadataService { make: exifTags.Make ?? null, model: exifTags.Model ?? null, fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), - iso: validate(exifTags.ISO), + iso: validate(exifTags.ISO) as number, exposureTime: exifTags.ExposureTime ?? null, lensModel: exifTags.LensModel ?? null, fNumber: validate(exifTags.FNumber), @@ -395,13 +396,13 @@ export class MetadataService { } private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { - const tags: Array = []; + const tags: string[] = []; if (exifTags.TagsList) { - tags.push(...exifTags.TagsList); + tags.push(...exifTags.TagsList.map(String)); } else if (exifTags.HierarchicalSubject) { tags.push( ...exifTags.HierarchicalSubject.map((tag) => - tag + String(tag) // convert | to / .replaceAll('/', '') .replaceAll('|', '/') @@ -413,10 +414,10 @@ export class MetadataService { if (!Array.isArray(keywords)) { keywords = [keywords]; } - tags.push(...keywords); + tags.push(...keywords.map(String)); } - const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) }); + const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) }); }