fix: time zone upserts (#25889)

This commit is contained in:
Daniel Dietzler 2026-02-05 18:43:03 +01:00 committed by GitHub
parent 27a2808470
commit 9c098109e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 69 additions and 7 deletions

View file

@ -473,6 +473,7 @@ describe('/asset', () => {
id: user1Assets[0].id, id: user1Assets[0].id,
exifInfo: expect.objectContaining({ exifInfo: expect.objectContaining({
dateTimeOriginal: '2023-11-20T01:11:00+00:00', dateTimeOriginal: '2023-11-20T01:11:00+00:00',
timeZone: 'UTC-7',
}), }),
}); });
expect(status).toEqual(200); expect(status).toEqual(200);

View file

@ -46,6 +46,7 @@ import {
onBeforeUnlink, onBeforeUnlink,
} from 'src/utils/asset.util'; } from 'src/utils/asset.util';
import { updateLockedColumns } from 'src/utils/database'; import { updateLockedColumns } from 'src/utils/database';
import { extractTimeZone } from 'src/utils/date';
import { transformOcrBoundingBox } from 'src/utils/transform'; import { transformOcrBoundingBox } from 'src/utils/transform';
@Injectable() @Injectable()
@ -168,12 +169,13 @@ export class AssetService extends BaseService {
}, },
_.isUndefined, _.isUndefined,
); );
const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined;
if (Object.keys(exifDto).length > 0) { if (Object.keys(exifDto).length > 0) {
await this.assetRepository.updateAllExif(ids, exifDto); await this.assetRepository.updateAllExif(ids, exifDto);
} }
const extractedTimeZone = extractTimeZone(dateTimeOriginal);
if ( if (
(dateTimeRelative !== undefined && dateTimeRelative !== 0) || (dateTimeRelative !== undefined && dateTimeRelative !== 0) ||
timeZone !== undefined || timeZone !== undefined ||
@ -513,12 +515,11 @@ export class AssetService extends BaseService {
rating?: number; rating?: number;
}) { }) {
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined;
const writes = _.omitBy( const writes = _.omitBy(
{ {
description, description,
dateTimeOriginal, dateTimeOriginal,
timeZone: extractedTimeZone?.type === 'fixed' ? extractedTimeZone.name : undefined, timeZone: extractTimeZone(dateTimeOriginal)?.name,
latitude, latitude,
longitude, longitude,
rating, rating,

View file

@ -1766,13 +1766,14 @@ describe(MetadataService.name, () => {
const asset = factory.jobAssets.sidecarWrite(); const asset = factory.jobAssets.sidecarWrite();
const description = 'this is a description'; const description = 'this is a description';
const gps = 12; const gps = 12;
const date = '2023-11-22T04:56:12.196Z'; const date = '2023-11-21T22:56:12.196-06:00';
mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([ mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([
'description', 'description',
'latitude', 'latitude',
'longitude', 'longitude',
'dateTimeOriginal', 'dateTimeOriginal',
'timeZone',
]); ]);
mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset); mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset);
await expect( await expect(
@ -1792,6 +1793,7 @@ describe(MetadataService.name, () => {
'latitude', 'latitude',
'longitude', 'longitude',
'dateTimeOriginal', 'dateTimeOriginal',
'timeZone',
]); ]);
}); });
}); });

View file

@ -32,6 +32,7 @@ import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types'; import { JobItem, JobOf } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util'; import { getAssetFiles } from 'src/utils/asset.util';
import { isAssetChecksumConstraint } from 'src/utils/database'; import { isAssetChecksumConstraint } from 'src/utils/database';
import { mergeTimeZone } from 'src/utils/date';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { isFaceImportEnabled } from 'src/utils/misc'; import { isFaceImportEnabled } from 'src/utils/misc';
import { upsertTags } from 'src/utils/tag'; import { upsertTags } from 'src/utils/tag';
@ -431,14 +432,16 @@ export class MetadataService extends BaseService {
const { sidecarFile } = getAssetFiles(asset.files); const { sidecarFile } = getAssetFiles(asset.files);
const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`; const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`;
const { description, dateTimeOriginal, latitude, longitude, rating, tags } = _.pick( const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick(
{ {
description: asset.exifInfo.description, description: asset.exifInfo.description,
dateTimeOriginal: asset.exifInfo.dateTimeOriginal, // the kysely type is wrong here; fixed in 0.28.3
dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null,
latitude: asset.exifInfo.latitude, latitude: asset.exifInfo.latitude,
longitude: asset.exifInfo.longitude, longitude: asset.exifInfo.longitude,
rating: asset.exifInfo.rating, rating: asset.exifInfo.rating,
tags: asset.exifInfo.tags, tags: asset.exifInfo.tags,
timeZone: asset.exifInfo.timeZone,
}, },
lockedProperties, lockedProperties,
); );
@ -447,7 +450,7 @@ export class MetadataService extends BaseService {
<Tags>{ <Tags>{
Description: description, Description: description,
ImageDescription: description, ImageDescription: description,
DateTimeOriginal: dateTimeOriginal ? String(dateTimeOriginal) : undefined, DateTimeOriginal: mergeTimeZone(dateTimeOriginal, timeZone)?.toISO(),
GPSLatitude: latitude, GPSLatitude: latitude,
GPSLongitude: longitude, GPSLongitude: longitude,
Rating: rating, Rating: rating,

View file

@ -1,3 +1,16 @@
import { DateTime } from 'luxon';
export const asDateString = (x: Date | string | null): string | null => { export const asDateString = (x: Date | string | null): string | null => {
return x instanceof Date ? x.toISOString().split('T')[0] : x; return x instanceof Date ? x.toISOString().split('T')[0] : x;
}; };
export const extractTimeZone = (dateTimeOriginal?: string | null) => {
const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined;
return extractedTimeZone?.type === 'fixed' ? extractedTimeZone : undefined;
};
export const mergeTimeZone = (dateTimeOriginal?: string | null, timeZone?: string | null) => {
return dateTimeOriginal
? DateTime.fromISO(dateTimeOriginal, { zone: 'UTC' }).setZone(timeZone ?? undefined)
: undefined;
};

View file

@ -456,6 +456,47 @@ describe(AssetService.name, () => {
); );
}); });
it('should relatively update an assets with timezone', async () => {
const { sut, ctx } = setup();
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
const { user } = await ctx.newUser();
const auth = factory.auth({ user });
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00', timeZone: 'UTC+5' });
await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -1441 });
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
expect.objectContaining({
exifInfo: expect.objectContaining({
dateTimeOriginal: '2023-11-18T18:10:00+00:00',
timeZone: 'UTC+5',
lockedProperties: ['timeZone', 'dateTimeOriginal'],
}),
}),
);
});
it('should relatively update an assets and set a timezone', async () => {
const { sut, ctx } = setup();
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
const { user } = await ctx.newUser();
const auth = factory.auth({ user });
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11, timeZone: 'UTC+5' });
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
expect.objectContaining({
exifInfo: expect.objectContaining({
dateTimeOriginal: '2023-11-19T18:00:00+00:00',
timeZone: 'UTC+5',
}),
}),
);
});
it('should update dateTimeOriginal', async () => { it('should update dateTimeOriginal', async () => {
const { sut, ctx } = setup(); const { sut, ctx } = setup();
ctx.getMock(JobRepository).queueAll.mockResolvedValue(); ctx.getMock(JobRepository).queueAll.mockResolvedValue();

View file

@ -361,6 +361,7 @@ const assetSidecarWriteFactory = () => {
latitude: 12, latitude: 12,
longitude: 12, longitude: 12,
dateTimeOriginal: '2023-11-22T04:56:12.196Z', dateTimeOriginal: '2023-11-22T04:56:12.196Z',
timeZone: 'UTC-6',
} as unknown as Exif, } as unknown as Exif,
}; };
}; };