pull/19178/merge
SGT 2025-12-10 18:13:11 +07:00 committed by GitHub
commit 04a7d85796
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 52 additions and 2 deletions

@ -26,6 +26,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const facesAssetDir = `${testAssetDir}/metadata/faces`;
const metaAssetFilepath = `${testAssetDir}/metadata/tags/picasa.jpg`;
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@ -54,6 +55,7 @@ describe('/asset', () => {
let user2Assets: AssetMediaResponseDto[];
let locationAsset: AssetMediaResponseDto;
let ratingAsset: AssetMediaResponseDto;
let metaAsset: AssetMediaResponseDto;
const setupTests = async () => {
await utils.resetDatabase();
@ -90,6 +92,16 @@ describe('/asset', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: ratingAsset.id });
// metadata asset
metaAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'picasa.jpg',
bytes: await readFile(metaAssetFilepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: metaAsset.id });
user1Assets = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
@ -649,6 +661,29 @@ describe('/asset', () => {
],
});
});
it('should be able to clear description', async () => {
// check original embedded description
let response = await utils.getAssetInfo(admin.accessToken, metaAsset.id);
expect(response.id).toEqual(metaAsset.id);
expect(response.exifInfo?.description).toEqual('Image-Description');
// clear description
const { status } = await request(app)
.put(`/assets/${metaAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ description: '' });
expect(status).toBe(200);
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: metaAsset.id });
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
// check if description is empty
response = await utils.getAssetInfo(admin.accessToken, metaAsset.id);
expect(response.id).toEqual(metaAsset.id);
expect(response.exifInfo?.description).toEqual('');
});
});
describe('DELETE /assets', () => {

@ -76,6 +76,8 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
@Injectable()
export class MetadataRepository {
private readonly baseWriteArgs = ['-api', 'largefilesupport=1', '-overwrite_original'];
private exiftool = new ExifTool({
defaultVideosToUTC: true,
backfillTimezones: true,
@ -88,7 +90,6 @@ export class MetadataRepository {
geolocation: true,
// Enable exiftool LFS to parse metadata for files larger than 2GB.
readArgs: ['-api', 'largefilesupport=1'],
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
});
constructor(private logger: LoggingRepository) {
@ -117,7 +118,21 @@ export class MetadataRepository {
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
try {
await this.exiftool.write(path, tags);
// handle special case for empty tags, otherwise exiftool will just remove them
const specialEmptyTags = ['Description', 'ImageDescription'];
// create a copy, to keep tags inmutable
const tagsToWrite: Partial<Tags> = { ...tags };
const rawArgs = [];
for (const tag of specialEmptyTags) {
if (Object.prototype.hasOwnProperty.call(tagsToWrite, tag)) {
const value = (tagsToWrite as any)[tag];
if (value === '') {
delete (tagsToWrite as any)[tag];
rawArgs.push(`-${tag}^=`);
}
}
}
await this.exiftool.write(path, tagsToWrite, { writeArgs: [...this.baseWriteArgs, ...rawArgs] });
} catch (error) {
this.logger.warn(`Error writing exif data (${path}): ${error}`);
}