fix(server): live photo match library (#32080)

(cherry picked from commit 05658ecba8679f54c4b95deeec3940b54ab0ecf7)
This commit is contained in:
Zack Pollard
2026-10-31 07:22:45 +03:00
committed by Codex
parent 92cf23b642
commit 91e416b104
2 changed files with 58 additions and 2 deletions
+2 -1
View File
@@ -698,13 +698,14 @@ export class AssetRepository {
} }
findLivePhotoMatch(options: LivePhotoSearchOptions) { findLivePhotoMatch(options: LivePhotoSearchOptions) {
const { ownerId, otherAssetId, livePhotoCID, type } = options; const { ownerId, libraryId, otherAssetId, livePhotoCID, type } = options;
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.select(['asset.id', 'asset.ownerId']) .select(['asset.id', 'asset.ownerId'])
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
.where('id', '!=', asUuid(otherAssetId)) .where('id', '!=', asUuid(otherAssetId))
.where('ownerId', '=', asUuid(ownerId)) .where('ownerId', '=', asUuid(ownerId))
.$call((qb) => (libraryId ? qb.where('libraryId', '=', asUuid(libraryId)) : qb.where('libraryId', 'is', null)))
.where('type', '=', type) .where('type', '=', type)
.where('asset_exif.livePhotoCID', '=', livePhotoCID) .where('asset_exif.livePhotoCID', '=', livePhotoCID)
.limit(1) .limit(1)
@@ -1,6 +1,7 @@
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { AssetFileType, AssetOrder, AssetOrderBy, AssetVisibility } from 'src/enum.js'; import { AssetFileType, AssetOrder, AssetOrderBy, AssetType, AssetVisibility } from 'src/enum.js';
import { AssetRepository } from 'src/repositories/asset.repository.js'; import { AssetRepository } from 'src/repositories/asset.repository.js';
import { LibraryRepository } from 'src/repositories/library.repository.js';
import { LoggingRepository } from 'src/repositories/logging.repository.js'; import { LoggingRepository } from 'src/repositories/logging.repository.js';
import { DB } from 'src/schema/index.js'; import { DB } from 'src/schema/index.js';
import { BaseService } from 'src/services/base.service.js'; import { BaseService } from 'src/services/base.service.js';
@@ -564,6 +565,60 @@ describe(AssetRepository.name, () => {
}); });
}); });
describe('findLivePhotoMatch', () => {
type LibraryKind = 'upload' | 'external' | 'other external';
const setupLivePhoto = async ({ still, motion }: { still: LibraryKind; motion: LibraryKind }) => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const newLibrary = () =>
ctx
.get(LibraryRepository)
.create({ ownerId: user.id, name: 'library', importPaths: [], exclusionPatterns: [] });
const [external, otherExternal] = await Promise.all([newLibrary(), newLibrary()]);
const libraryIds = { upload: null, external: external.id, 'other external': otherExternal.id };
const livePhotoCID = 'live-photo-cid';
const [{ asset: stillAsset }, { asset: motionAsset }] = await Promise.all([
ctx.newAsset({ ownerId: user.id, type: AssetType.Image, libraryId: libraryIds[still] }),
ctx.newAsset({ ownerId: user.id, type: AssetType.Video, libraryId: libraryIds[motion] }),
]);
await Promise.all([
ctx.newExif({ assetId: stillAsset.id, livePhotoCID }),
ctx.newExif({ assetId: motionAsset.id, livePhotoCID }),
]);
const match = await sut.findLivePhotoMatch({
ownerId: user.id,
libraryId: libraryIds[still],
livePhotoCID,
otherAssetId: stillAsset.id,
type: AssetType.Video,
});
return { match, motionAsset };
};
it.each<LibraryKind>(['upload', 'external'])(
'should match a motion video in the same %s library',
async (library) => {
const { match, motionAsset } = await setupLivePhoto({ still: library, motion: library });
expect(match).toEqual({ id: motionAsset.id, ownerId: motionAsset.ownerId });
},
);
it.each<{ still: LibraryKind; motion: LibraryKind }>([
{ still: 'upload', motion: 'external' },
{ still: 'external', motion: 'upload' },
{ still: 'external', motion: 'other external' },
])('should not match a still in the $still library to a motion video in the $motion library', async (libraries) => {
const { match } = await setupLivePhoto(libraries);
expect(match).toBeUndefined();
});
});
describe('createAll', () => { describe('createAll', () => {
it('should return an empty array when given an empty input', async () => { it('should return an empty array when given an empty input', async () => {
const { sut } = setup(); const { sut } = setup();