mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	fix(server): handle numeric hierarchical subject values (#12949)
This commit is contained in:
		
							parent
							
								
									62a490eca2
								
							
						
					
					
						commit
						b6f871786c
					
				@ -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<Tags, TagsWithWrongTypes> {
 | 
			
		||||
  ContentIdentifier?: string;
 | 
			
		||||
  MotionPhoto?: number;
 | 
			
		||||
@ -20,10 +31,14 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
 | 
			
		||||
  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?: {
 | 
			
		||||
 | 
			
		||||
@ -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'] });
 | 
			
		||||
 | 
			
		||||
@ -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<ExifEntity> = {
 | 
			
		||||
      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<string | number> = [];
 | 
			
		||||
    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('/', '<PLACEHOLDER>')
 | 
			
		||||
            .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) });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user