mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 01:11:36 +03:00
refactor(server): link live photos as part of metadata extraction instead of queueing job (#16390)
* link live photos helper instead of job * update test * queue storage template migration * queue in onDone * remove link live photos job
This commit is contained in:
@@ -76,125 +76,6 @@ describe(MetadataService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleLivePhotoLinking', () => {
|
||||
it('should handle an asset that could not be found', async () => {
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle an asset without exif info', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle livePhotoCID not set', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image }]);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle not finding a match', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoMotionAsset,
|
||||
exifInfo: { livePhotoCID: assetStub.livePhotoStillAsset.id } as ExifEntity,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
||||
JobStatus.SKIPPED,
|
||||
);
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
livePhotoCID: assetStub.livePhotoStillAsset.id,
|
||||
ownerId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
||||
type: AssetType.IMAGE,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalled();
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should link photo and video', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoStillAsset,
|
||||
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
||||
},
|
||||
]);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||
JobStatus.SUCCESS,
|
||||
);
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
livePhotoCID: assetStub.livePhotoMotionAsset.id,
|
||||
ownerId: assetStub.livePhotoStillAsset.ownerId,
|
||||
otherAssetId: assetStub.livePhotoStillAsset.id,
|
||||
type: AssetType.VIDEO,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
});
|
||||
|
||||
it('should notify clients on live photo link', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoStillAsset,
|
||||
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
||||
},
|
||||
]);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||
JobStatus.SUCCESS,
|
||||
);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
userId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should search by libraryId', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoStillAsset,
|
||||
libraryId: 'library-id',
|
||||
exifInfo: { livePhotoCID: 'CID' } as ExifEntity,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||
JobStatus.SKIPPED,
|
||||
);
|
||||
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
ownerId: 'user-id',
|
||||
otherAssetId: 'live-photo-still-asset',
|
||||
livePhotoCID: 'CID',
|
||||
libraryId: 'library-id',
|
||||
type: 'VIDEO',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueMetadataExtraction', () => {
|
||||
it('should queue metadata extraction for all assets without exif values', async () => {
|
||||
mocks.asset.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
||||
@@ -746,11 +627,11 @@ describe(MetadataService.name, () => {
|
||||
mocks.storage.checkFileExists.mockResolvedValue(true);
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
expect(mocks.asset.create).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.storage.createOrOverwriteFile).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.asset.create).not.toHaveBeenCalled();
|
||||
expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled();
|
||||
// The still asset gets saved by handleMetadataExtraction, but not the video
|
||||
expect(mocks.asset.update).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.job.queue).toHaveBeenCalledTimes(0);
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => {
|
||||
@@ -1178,6 +1059,107 @@ describe(MetadataService.name, () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle livePhotoCID not set', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } });
|
||||
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle not finding a match', async () => {
|
||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
||||
JobStatus.SUCCESS,
|
||||
);
|
||||
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], {
|
||||
faces: { person: false },
|
||||
});
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
livePhotoCID: 'CID',
|
||||
ownerId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
otherAssetId: assetStub.livePhotoMotionAsset.id,
|
||||
type: AssetType.IMAGE,
|
||||
});
|
||||
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
|
||||
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should link photo and video', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||
JobStatus.SUCCESS,
|
||||
);
|
||||
|
||||
expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], {
|
||||
faces: { person: false },
|
||||
});
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
livePhotoCID: 'CID',
|
||||
ownerId: assetStub.livePhotoStillAsset.ownerId,
|
||||
otherAssetId: assetStub.livePhotoStillAsset.id,
|
||||
type: AssetType.VIDEO,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({
|
||||
id: assetStub.livePhotoStillAsset.id,
|
||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
|
||||
expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
|
||||
});
|
||||
|
||||
it('should notify clients on live photo link', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoStillAsset,
|
||||
exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity,
|
||||
},
|
||||
]);
|
||||
mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||
JobStatus.SUCCESS,
|
||||
);
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
userId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should search by libraryId', async () => {
|
||||
mocks.asset.getByIds.mockResolvedValue([
|
||||
{
|
||||
...assetStub.livePhotoStillAsset,
|
||||
libraryId: 'library-id',
|
||||
},
|
||||
]);
|
||||
mockReadTags({ ContentIdentifier: 'CID' });
|
||||
|
||||
await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||
JobStatus.SUCCESS,
|
||||
);
|
||||
|
||||
expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||
ownerId: 'user-id',
|
||||
otherAssetId: 'live-photo-still-asset',
|
||||
livePhotoCID: 'CID',
|
||||
libraryId: 'library-id',
|
||||
type: 'VIDEO',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleQueueSidecar', () => {
|
||||
|
||||
Reference in New Issue
Block a user