mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(server): Update XMP sidecar search to look for both photo.ext.xmp and photo.xmp (#7813)
* Add support for photo.xmp sidecars * format * Add comment * Proper handling * Handle mocking better * Address PR feedback * Add test coverage if both xmp files exist * Update server/src/domain/metadata/metadata.service.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * Update server/src/domain/metadata/metadata.service.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * Update server/src/domain/metadata/metadata.service.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> --------- Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
37e5b91dc2
commit
29c3a826c5
@ -646,7 +646,7 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set sidecar path if exists', async () => {
|
it('should set sidecar path if exists (sidecar named photo.ext.xmp)', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
@ -658,6 +658,41 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set sidecar path if exists (sidecar named photo.xmp)', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecarWithoutExt]);
|
||||||
|
storageMock.checkFileExists.mockResolvedValueOnce(false);
|
||||||
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(true);
|
||||||
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
|
constants.R_OK,
|
||||||
|
);
|
||||||
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
|
id: assetStub.sidecarWithoutExt.id,
|
||||||
|
sidecarPath: assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set sidecar path if exists (two sidecars named photo.ext.xmp and photo.xmp, should pick photo.ext.xmp)', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true);
|
||||||
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK);
|
||||||
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
|
constants.R_OK,
|
||||||
|
);
|
||||||
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
|
id: assetStub.sidecar.id,
|
||||||
|
sidecarPath: assetStub.sidecar.sidecarPath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should unset sidecar path if file does not exist anymore', async () => {
|
it('should unset sidecar path if file does not exist anymore', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
storageMock.checkFileExists.mockResolvedValue(false);
|
storageMock.checkFileExists.mockResolvedValue(false);
|
||||||
|
@ -6,6 +6,7 @@ import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Duration } from 'luxon';
|
import { Duration } from 'luxon';
|
||||||
import { constants } from 'node:fs/promises';
|
import { constants } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { handlePromiseError, usePagination } from '../domain.util';
|
import { handlePromiseError, usePagination } from '../domain.util';
|
||||||
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
||||||
@ -566,9 +567,25 @@ export class MetadataService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidecarPath = `${asset.originalPath}.xmp`;
|
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||||
const exists = await this.storageRepository.checkFileExists(sidecarPath, constants.R_OK);
|
const assetPath = path.parse(asset.originalPath);
|
||||||
if (exists) {
|
const assetPathWithoutExt = path.join(assetPath.dir, assetPath.name);
|
||||||
|
const sidecarPathWithoutExt = `${assetPathWithoutExt}.xmp`;
|
||||||
|
const sidecarPathWithExt = `${asset.originalPath}.xmp`;
|
||||||
|
|
||||||
|
const [sidecarPathWithExtExists, sidecarPathWithoutExtExists] = await Promise.all([
|
||||||
|
this.storageRepository.checkFileExists(sidecarPathWithExt, constants.R_OK),
|
||||||
|
this.storageRepository.checkFileExists(sidecarPathWithoutExt, constants.R_OK),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let sidecarPath = null;
|
||||||
|
if (sidecarPathWithExtExists) {
|
||||||
|
sidecarPath = sidecarPathWithExt;
|
||||||
|
} else if (sidecarPathWithoutExtExists) {
|
||||||
|
sidecarPath = sidecarPathWithoutExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidecarPath) {
|
||||||
await this.assetRepository.save({ id: asset.id, sidecarPath });
|
await this.assetRepository.save({ id: asset.id, sidecarPath });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -577,7 +594,9 @@ export class MetadataService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Sidecar File '${sidecarPath}' was not found, removing sidecarPath for asset ${asset.id}`);
|
this.logger.debug(
|
||||||
|
`Sidecar file was not found. Checked paths '${sidecarPathWithExt}' and '${sidecarPathWithoutExt}'. Removing sidecarPath for asset ${asset.id}`,
|
||||||
|
);
|
||||||
await this.assetRepository.save({ id: asset.id, sidecarPath: null });
|
await this.assetRepository.save({ id: asset.id, sidecarPath: null });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
36
server/test/fixtures/asset.stub.ts
vendored
36
server/test/fixtures/asset.stub.ts
vendored
@ -524,6 +524,42 @@ export const assetStub = {
|
|||||||
sidecarPath: '/original/path.ext.xmp',
|
sidecarPath: '/original/path.ext.xmp',
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
}),
|
}),
|
||||||
|
sidecarWithoutExt: Object.freeze<AssetEntity>({
|
||||||
|
id: 'asset-id',
|
||||||
|
deviceAssetId: 'device-asset-id',
|
||||||
|
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
owner: userStub.user1,
|
||||||
|
ownerId: 'user-id',
|
||||||
|
deviceId: 'device-id',
|
||||||
|
originalPath: '/original/path.ext',
|
||||||
|
resizePath: '/uploads/user-id/thumbs/path.ext',
|
||||||
|
thumbhash: null,
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
webpPath: null,
|
||||||
|
encodedVideoPath: null,
|
||||||
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
isFavorite: true,
|
||||||
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isExternal: false,
|
||||||
|
isOffline: false,
|
||||||
|
libraryId: 'library-id',
|
||||||
|
library: libraryStub.uploadLibrary1,
|
||||||
|
duration: null,
|
||||||
|
isVisible: true,
|
||||||
|
livePhotoVideo: null,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
originalFileName: 'asset-id.ext',
|
||||||
|
faces: [],
|
||||||
|
sidecarPath: '/original/path.xmp',
|
||||||
|
deletedAt: null,
|
||||||
|
}),
|
||||||
|
|
||||||
readOnly: Object.freeze<AssetEntity>({
|
readOnly: Object.freeze<AssetEntity>({
|
||||||
id: 'read-only-asset',
|
id: 'read-only-asset',
|
||||||
|
Loading…
Reference in New Issue
Block a user