From 79f978ddebcac0686fb26d26c4e424ef60fe0390 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:17:56 +0100 Subject: [PATCH] fix: writing empty exif tags (#27025) --- .../src/repositories/metadata.repository.ts | 6 +- server/src/services/metadata.service.ts | 2 +- .../repositories/metadata.repository.spec.ts | 75 +++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 server/test/medium/specs/repositories/metadata.repository.spec.ts diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index fc00d44b3f..57c688cac2 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -119,8 +119,12 @@ export class MetadataRepository { } async writeTags(path: string, tags: Partial): Promise { + // If exiftool assigns a field with ^= instead of =, empty values will be written too. + // Since exiftool-vendored doesn't support an option for this, we append the ^ to the name of the tag instead. + // https://exiftool.org/exiftool_pod.html#:~:text=is%20used%20to%20write%20an%20empty%20string + const tagsToWrite = Object.fromEntries(Object.entries(tags).map(([key, value]) => [`${key}^`, value])); try { - await this.exiftool.write(path, tags); + await this.exiftool.write(path, tagsToWrite); } catch (error) { this.logger.warn(`Error writing exif data (${path}): ${error}`); } diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index d2467ae6d9..7b87ea06a6 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -467,7 +467,7 @@ export class MetadataService extends BaseService { GPSLatitude: latitude, GPSLongitude: longitude, Rating: rating, - TagsList: tags?.length ? tags : undefined, + TagsList: tags, }, _.isUndefined, ); diff --git a/server/test/medium/specs/repositories/metadata.repository.spec.ts b/server/test/medium/specs/repositories/metadata.repository.spec.ts new file mode 100644 index 0000000000..976f33695e --- /dev/null +++ b/server/test/medium/specs/repositories/metadata.repository.spec.ts @@ -0,0 +1,75 @@ +import { Kysely } from 'kysely'; +import { mkdtempSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { newDate } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let database: Kysely; + +const setup = () => { + const { ctx } = newMediumService(BaseService, { + database, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(MetadataRepository) }; +}; + +beforeAll(async () => { + database = await getKyselyDB(); +}); + +describe(MetadataRepository.name, () => { + describe('writeTags', () => { + it('should write an empty description', async () => { + const { sut } = setup(); + const dir = mkdtempSync(join(tmpdir(), 'metadata-medium-write-tags')); + const sidecarFile = join(dir, 'sidecar.xmp'); + + await sut.writeTags(sidecarFile, { Description: '' }); + expect(readFileSync(sidecarFile).toString()).toEqual(expect.stringContaining('rdf:Description')); + }); + + it('should write an empty tags list', async () => { + const { sut } = setup(); + const dir = mkdtempSync(join(tmpdir(), 'metadata-medium-write-tags')); + const sidecarFile = join(dir, 'sidecar.xmp'); + + await sut.writeTags(sidecarFile, { TagsList: [] }); + const fileContent = readFileSync(sidecarFile).toString(); + expect(fileContent).toEqual(expect.stringContaining('digiKam:TagsList')); + expect(fileContent).toEqual(expect.stringContaining('')); + }); + }); + + it('should write tags', async () => { + const { sut } = setup(); + const dir = mkdtempSync(join(tmpdir(), 'metadata-medium-write-tags')); + const sidecarFile = join(dir, 'sidecar.xmp'); + + await sut.writeTags(sidecarFile, { + Description: 'my-description', + ImageDescription: 'my-image-description', + DateTimeOriginal: newDate().toISOString(), + GPSLatitude: 42, + GPSLongitude: 69, + Rating: 3, + TagsList: ['tagA'], + }); + + const fileContent = readFileSync(sidecarFile).toString(); + expect(fileContent).toEqual(expect.stringContaining('my-description')); + expect(fileContent).toEqual(expect.stringContaining('my-image-description')); + expect(fileContent).toEqual(expect.stringContaining('exif:DateTimeOriginal')); + expect(fileContent).toEqual(expect.stringContaining('42,0.0N')); + expect(fileContent).toEqual(expect.stringContaining('69,0.0E')); + expect(fileContent).toEqual(expect.stringContaining('3')); + expect(fileContent).toEqual(expect.stringContaining('tagA')); + }); +});