From 7ca53ba5077136c853c2b28fce1e0ad9d331e591 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 3 Sep 2024 18:25:09 -0400 Subject: [PATCH] feat(server): support lightroom tags (#12288) --- server/src/services/metadata.service.spec.ts | 45 ++++++++++++++++++++ server/src/services/metadata.service.ts | 10 +++++ 2 files changed, 55 insertions(+) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 84b67be5cd..2fc95df00e 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -449,6 +449,51 @@ describe(MetadataService.name, () => { }); }); + it('should extract hierarchy from HierarchicalSubject', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent|Child'] }); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + + it('should extract ignore / characters in a HierarchicalSubject tag', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] }); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ + userId: 'user-id', + value: 'Mom|Dad', + parent: undefined, + }); + }); + + it('should ignore HierarchicalSubject when TagsList is present', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + it('should not apply motion photos if asset is video', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index de3babb138..29aebc4a36 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -355,6 +355,16 @@ export class MetadataService { const tags: unknown[] = []; if (exifTags.TagsList) { tags.push(...exifTags.TagsList); + } else if (exifTags.HierarchicalSubject) { + tags.push( + exifTags.HierarchicalSubject.map((tag) => + tag + // convert | to / + .replaceAll('/', '') + .replaceAll('|', '/') + .replaceAll('', '|'), + ), + ); } else if (exifTags.Keywords) { let keywords = exifTags.Keywords; if (!Array.isArray(keywords)) {