This commit is contained in:
mertalev
2024-12-18 15:27:31 -05:00
parent 007caa26bd
commit 38a82d39d3
48 changed files with 2702 additions and 3164 deletions
@@ -23,7 +23,6 @@ import { fileStub } from 'test/fixtures/file.stub';
import { userStub } from 'test/fixtures/user.stub';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
import { newTestService } from 'test/utils';
import { QueryFailedError } from 'typeorm';
import { Mocked } from 'vitest';
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
@@ -370,8 +369,8 @@ describe(AssetMediaService.name, () => {
originalName: 'asset_1.jpeg',
size: 0,
};
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
const error = new Error('unique key violation');
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
assetMock.create.mockRejectedValue(error);
assetMock.getUploadAssetIdByChecksum.mockResolvedValue(assetEntity.id);
@@ -397,8 +396,8 @@ describe(AssetMediaService.name, () => {
originalName: 'asset_1.jpeg',
size: 0,
};
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
const error = new Error('unique key violation');
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
assetMock.create.mockRejectedValue(error);
@@ -480,7 +479,6 @@ describe(AssetMediaService.name, () => {
it('should throw an error if the asset is not found', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getById.mockResolvedValue(null);
await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException);
@@ -512,7 +510,6 @@ describe(AssetMediaService.name, () => {
it('should throw an error if the asset does not exist', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(null);
await expect(
sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }),
@@ -618,7 +615,6 @@ describe(AssetMediaService.name, () => {
it('should throw an error if the asset does not exist', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
assetMock.getById.mockResolvedValue(null);
await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException);
});
@@ -670,8 +666,6 @@ describe(AssetMediaService.name, () => {
describe('replaceAsset', () => {
it('should error when update photo does not exist', async () => {
assetMock.getById.mockResolvedValueOnce(null);
await expect(sut.replaceAsset(authStub.user1, 'id', replaceDto, fileStub.photo)).rejects.toThrow(
'Not found or no asset.update access',
);
@@ -785,8 +779,8 @@ describe(AssetMediaService.name, () => {
it('should handle a photo with sidecar to duplicate photo ', async () => {
const updatedFile = fileStub.photo;
const error = new QueryFailedError('', [], new Error('unique key violation'));
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
const error = new Error('unique key violation');
(error as any).constraint_name = ASSET_CHECKSUM_CONSTRAINT;
assetMock.update.mockRejectedValue(error);
assetMock.getById.mockResolvedValueOnce(sidecarAsset);
+2 -3
View File
@@ -30,7 +30,6 @@ import { asRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util';
import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file';
import { mimeTypes } from 'src/utils/mime-types';
import { fromChecksum } from 'src/utils/request';
import { QueryFailedError } from 'typeorm';
export interface UploadRequest {
auth: AuthDto | null;
fieldName: UploadFieldName;
@@ -302,7 +301,7 @@ export class AssetMediaService extends BaseService {
});
// handle duplicates with a success response
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
if (error.constraint_name === ASSET_CHECKSUM_CONSTRAINT) {
const duplicateId = await this.assetRepository.getUploadAssetIdByChecksum(auth.user.id, file.checksum);
if (!duplicateId) {
this.logger.error(`Error locating duplicate for checksum constraint`);
@@ -343,7 +342,7 @@ export class AssetMediaService extends BaseService {
localDateTime: dto.fileCreatedAt,
duration: dto.duration || null,
livePhotoVideo: null,
livePhotoVideoId: null,
sidecarPath: sidecarPath || null,
});
+15 -9
View File
@@ -51,9 +51,7 @@ describe(AssetService.name, () => {
});
const mockGetById = (assets: AssetEntity[]) => {
assetMock.getById.mockImplementation((assetId) =>
Promise.resolve(assets.find((asset) => asset.id === assetId) ?? null),
);
assetMock.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId)));
};
beforeEach(() => {
@@ -250,27 +248,34 @@ describe(AssetService.name, () => {
it('should update the asset', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getById.mockResolvedValue(assetStub.image);
assetMock.update.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
});
it('should update the exif description', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getById.mockResolvedValue(assetStub.image);
assetMock.update.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
});
it('should update the exif rating', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
assetMock.getById.mockResolvedValue(assetStub.image);
assetMock.getById.mockResolvedValueOnce(assetStub.image);
assetMock.update.mockResolvedValueOnce(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { rating: 3 });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', rating: 3 });
});
it('should fail linking a live video if the motion part could not be found', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValue(null);
await expect(
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
@@ -339,6 +344,7 @@ describe(AssetService.name, () => {
isVisible: true,
});
assetMock.getById.mockResolvedValueOnce(assetStub.image);
assetMock.update.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
@@ -366,7 +372,7 @@ describe(AssetService.name, () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
assetMock.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
assetMock.getById.mockResolvedValueOnce(assetStub.image);
assetMock.update.mockResolvedValueOnce(assetStub.image);
await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null });
@@ -383,15 +389,15 @@ describe(AssetService.name, () => {
it('should fail unlinking a live video if the asset could not be found', async () => {
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id]));
assetMock.getById.mockResolvedValue(null);
// eslint-disable-next-line unicorn/no-useless-undefined
assetMock.getById.mockResolvedValueOnce(undefined);
await expect(
sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }),
).rejects.toBeInstanceOf(BadRequestException);
expect(assetMock.update).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
expect(eventMock.emit).not.toHaveBeenCalledWith();
expect(eventMock.emit).not.toHaveBeenCalled();
});
});
+9 -37
View File
@@ -74,29 +74,13 @@ export class AssetService extends BaseService {
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [id] });
const asset = await this.assetRepository.getById(
id,
{
exifInfo: true,
sharedLinks: true,
tags: true,
owner: true,
faces: {
person: true,
},
stack: {
assets: {
exifInfo: true,
},
},
files: true,
},
{
faces: {
boundingBoxX1: 'ASC',
},
},
);
const asset = await this.assetRepository.getById(id, {
exifInfo: true,
owner: true,
faces: { person: true },
stack: { assets: true },
tags: true,
});
if (!asset) {
throw new BadRequestException('Asset not found');
@@ -137,22 +121,12 @@ export class AssetService extends BaseService {
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
await this.assetRepository.update({ id, ...rest });
const asset = await this.assetRepository.update({ id, ...rest });
if (previousMotion) {
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
}
const asset = await this.assetRepository.getById(id, {
exifInfo: true,
owner: true,
tags: true,
faces: {
person: true,
},
files: true,
});
if (!asset) {
throw new BadRequestException('Asset not found');
}
@@ -202,9 +176,7 @@ export class AssetService extends BaseService {
const { id, deleteOnDisk } = job;
const asset = await this.assetRepository.getById(id, {
faces: {
person: true,
},
faces: { person: true },
library: true,
stack: { assets: true },
exifInfo: true,
+2 -4
View File
@@ -71,10 +71,8 @@ export class BackupService extends BaseService {
@OnJob({ name: JobName.BACKUP_DATABASE, queue: QueueName.BACKUP_DATABASE })
async handleBackupDatabase(): Promise<JobStatus> {
this.logger.debug(`Database Backup Started`);
const {
database: { config },
} = this.configRepository.getEnv();
const { database } = this.configRepository.getEnv();
const config = database.config.typeorm;
const isUrlConnection = config.connectionType === 'url';
+40 -21
View File
@@ -1,3 +1,4 @@
import { PostgresJSDialect } from 'kysely-postgres-js';
import { IConfigRepository } from 'src/interfaces/config.interface';
import {
DatabaseExtension,
@@ -61,13 +62,19 @@ describe(DatabaseService.name, () => {
mockEnvData({
database: {
config: {
connectionType: 'parts',
type: 'postgres',
host: 'database',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'immich',
kysely: {
dialect: expect.any(PostgresJSDialect),
log: ['error'],
},
typeorm: {
connectionType: 'parts',
type: 'postgres',
host: 'database',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'immich',
},
},
skipMigrations: false,
vectorExtension: extension,
@@ -291,13 +298,19 @@ describe(DatabaseService.name, () => {
mockEnvData({
database: {
config: {
connectionType: 'parts',
type: 'postgres',
host: 'database',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'immich',
kysely: {
dialect: expect.any(PostgresJSDialect),
log: ['error'],
},
typeorm: {
connectionType: 'parts',
type: 'postgres',
host: 'database',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'immich',
},
},
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTORS,
@@ -315,13 +328,19 @@ describe(DatabaseService.name, () => {
mockEnvData({
database: {
config: {
connectionType: 'parts',
type: 'postgres',
host: 'database',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'immich',
kysely: {
dialect: expect.any(PostgresJSDialect),
log: ['error'],
},
typeorm: {
connectionType: 'parts',
type: 'postgres',
host: 'database',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'immich',
},
},
skipMigrations: true,
vectorExtension: DatabaseExtension.VECTOR,
@@ -31,7 +31,12 @@ describe(SearchService.name, () => {
describe('getDuplicates', () => {
it('should get duplicates', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe, assetStub.hasDupe]);
assetMock.getDuplicates.mockResolvedValue([
{
duplicateId: assetStub.hasDupe.duplicateId!,
assets: [assetStub.hasDupe, assetStub.hasDupe],
},
]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
{
duplicateId: assetStub.hasDupe.duplicateId,
@@ -42,12 +47,6 @@ describe(SearchService.name, () => {
},
]);
});
it('should update assets with duplicateId', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([]);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasDupe.id], { duplicateId: null });
});
});
describe('handleQueueSearchDuplicates', () => {
+6 -20
View File
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { OnJob } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
@@ -15,25 +15,11 @@ import { usePagination } from 'src/utils/pagination';
@Injectable()
export class DuplicateService extends BaseService {
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
const uniqueAssetIds: string[] = [];
const duplicates = mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))).filter(
(duplicate) => {
if (duplicate.assets.length === 1) {
uniqueAssetIds.push(duplicate.assets[0].id);
return false;
}
return true;
},
);
if (uniqueAssetIds.length > 0) {
try {
await this.assetRepository.updateAll(uniqueAssetIds, { duplicateId: null });
} catch (error: any) {
this.logger.error(`Failed to remove duplicateId from assets: ${error.message}`);
}
}
return duplicates;
const duplicates = await this.assetRepository.getDuplicates(auth.user.id);
return duplicates.map(({ duplicateId, assets }) => ({
duplicateId,
assets: assets.map((asset) => mapAsset(asset, { auth })),
}));
}
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
@@ -256,8 +256,6 @@ describe(LibraryService.name, () => {
exclusionPatterns: [],
};
assetMock.getById.mockResolvedValue(null);
await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED);
expect(assetMock.remove).not.toHaveBeenCalled();
@@ -394,7 +392,6 @@ describe(LibraryService.name, () => {
assetPath: '/data/user1/photo.jpg',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
@@ -438,7 +435,6 @@ describe(LibraryService.name, () => {
assetPath: '/data/user1/video.mp4',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.video);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
@@ -482,7 +478,6 @@ describe(LibraryService.name, () => {
assetPath: '/data/user1/photo.jpg',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
@@ -548,7 +543,6 @@ describe(LibraryService.name, () => {
assetPath: '/data/user1/photo.jpg',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
@@ -567,7 +561,6 @@ describe(LibraryService.name, () => {
assetPath: '/data/user1/photo.jpg',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
+4 -7
View File
@@ -2,6 +2,7 @@ import { BinaryField, ExifDateTime } from 'exiftool-vendored';
import { randomBytes } from 'node:crypto';
import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises';
import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { AssetType, ExifOrientation, ImmichWorker, SourceType } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
@@ -200,7 +201,6 @@ describe(MetadataService.name, () => {
exifInfo: { livePhotoCID: 'CID' } as ExifEntity,
},
]);
assetMock.findLivePhotoMatch.mockResolvedValue(null);
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
JobStatus.SKIPPED,
@@ -579,7 +579,6 @@ describe(MetadataService.name, () => {
EmbeddedVideoType: 'MotionPhoto_Data',
});
cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
const video = randomBytes(512);
@@ -624,7 +623,6 @@ describe(MetadataService.name, () => {
EmbeddedVideoType: 'MotionPhoto_Data',
});
cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
const video = randomBytes(512);
@@ -670,7 +668,6 @@ describe(MetadataService.name, () => {
MicroVideoOffset: 1,
});
cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
cryptoMock.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid);
const video = randomBytes(512);
@@ -716,8 +713,9 @@ describe(MetadataService.name, () => {
MicroVideoOffset: 1,
});
cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockImplementation((asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }));
assetMock.create.mockImplementation(
(asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise<AssetEntity>,
);
const video = randomBytes(512);
storageMock.readFile.mockResolvedValue(video);
@@ -789,7 +787,6 @@ describe(MetadataService.name, () => {
MicroVideoOffset: 1,
});
cryptoMock.hashSha1.mockReturnValue(randomBytes(512));
assetMock.getByChecksum.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.livePhotoMotionAsset);
const video = randomBytes(512);
storageMock.readFile.mockResolvedValue(video);
+3 -2
View File
@@ -1,16 +1,17 @@
import { Injectable } from '@nestjs/common';
import { ContainerDirectoryItem, ExifDateTime, Maybe, Tags } from 'exiftool-vendored';
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
import { Insertable } from 'kysely';
import _ from 'lodash';
import { Duration } from 'luxon';
import { constants } from 'node:fs/promises';
import path from 'node:path';
import { SystemConfig } from 'src/config';
import { StorageCore } from 'src/cores/storage.core';
import { Exif } from 'src/db';
import { OnEvent, OnJob } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { AssetType, ExifOrientation, ImmichWorker, SourceType } from 'src/enum';
import { WithoutProperty } from 'src/interfaces/asset.interface';
@@ -166,7 +167,7 @@ export class MetadataService extends BaseService {
const { width, height } = this.getImageDimensions(exifTags);
const exifData: Partial<ExifEntity> = {
const exifData: Insertable<Exif> = {
assetId: asset.id,
// dates
+14 -12
View File
@@ -728,11 +728,13 @@ describe(PersonService.name, () => {
assetId: assetStub.image.id,
facesRecognizedAt: expect.any(Date),
});
expect(assetMock.upsertJobStatus.mock.calls[0][0].facesRecognizedAt?.getTime()).toBeGreaterThan(start);
const facesRecognizedAt = assetMock.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date;
expect(facesRecognizedAt.getTime()).toBeGreaterThan(start);
});
it('should create a face with no person and queue recognition job', async () => {
machineLearningMock.detectFaces.mockResolvedValue(detectFaceMock);
searchMock.searchFaces.mockResolvedValue([{ ...faceStub.face1, distance: 0.7 }]);
assetMock.getByIds.mockResolvedValue([assetStub.image]);
await sut.handleDetectFaces({ id: assetStub.image.id });
@@ -840,10 +842,10 @@ describe(PersonService.name, () => {
}
const faces = [
{ face: faceStub.noPerson1, distance: 0 },
{ face: faceStub.primaryFace1, distance: 0.2 },
{ face: faceStub.noPerson2, distance: 0.3 },
{ face: faceStub.face1, distance: 0.4 },
{ ...faceStub.noPerson1, distance: 0 },
{ ...faceStub.primaryFace1, distance: 0.2 },
{ ...faceStub.noPerson2, distance: 0.3 },
{ ...faceStub.face1, distance: 0.4 },
] as FaceSearchResult[];
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
@@ -867,8 +869,8 @@ describe(PersonService.name, () => {
it('should create a new person if the face is a core point with no person', async () => {
const faces = [
{ face: faceStub.noPerson1, distance: 0 },
{ face: faceStub.noPerson2, distance: 0.3 },
{ ...faceStub.noPerson1, distance: 0 },
{ ...faceStub.noPerson2, distance: 0.3 },
] as FaceSearchResult[];
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
@@ -889,7 +891,7 @@ describe(PersonService.name, () => {
});
it('should not queue face with no matches', async () => {
const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
const faces = [{ ...faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
searchMock.searchFaces.mockResolvedValue(faces);
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
@@ -905,8 +907,8 @@ describe(PersonService.name, () => {
it('should defer non-core faces to end of queue', async () => {
const faces = [
{ face: faceStub.noPerson1, distance: 0 },
{ face: faceStub.noPerson2, distance: 0.4 },
{ ...faceStub.noPerson1, distance: 0 },
{ ...faceStub.noPerson2, distance: 0.4 },
] as FaceSearchResult[];
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
@@ -927,8 +929,8 @@ describe(PersonService.name, () => {
it('should not assign person to deferred non-core face with no matching person', async () => {
const faces = [
{ face: faceStub.noPerson1, distance: 0 },
{ face: faceStub.noPerson2, distance: 0.4 },
{ ...faceStub.noPerson1, distance: 0 },
{ ...faceStub.noPerson2, distance: 0.4 },
] as FaceSearchResult[];
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
+4 -10
View File
@@ -261,7 +261,7 @@ export class PersonService extends BaseService {
return force === false
? this.assetRepository.getWithout(pagination, WithoutProperty.FACES)
: this.assetRepository.getAll(pagination, {
orderDirection: 'DESC',
orderDirection: 'desc',
withFaces: true,
withArchived: true,
isVisible: true,
@@ -288,13 +288,7 @@ export class PersonService extends BaseService {
return JobStatus.SKIPPED;
}
const relations = {
exifInfo: true,
faces: {
person: false,
},
files: true,
};
const relations = { exifInfo: true, faces: { person: false }, files: true };
const [asset] = await this.assetRepository.getByIds([id], relations);
const { previewFile } = getAssetFiles(asset.files);
if (!asset || !previewFile) {
@@ -491,7 +485,7 @@ export class PersonService extends BaseService {
return JobStatus.SKIPPED;
}
let personId = matches.find((match) => match.face.personId)?.face.personId;
let personId = matches.find((match) => match.personId)?.personId;
if (!personId) {
const matchWithPerson = await this.searchRepository.searchFaces({
userIds: [face.asset.ownerId],
@@ -502,7 +496,7 @@ export class PersonService extends BaseService {
});
if (matchWithPerson.length > 0) {
personId = matchWithPerson[0].face.personId;
personId = matchWithPerson[0].personId;
}
}
+3 -3
View File
@@ -45,11 +45,11 @@ describe(SearchService.name, () => {
it('should get assets by city and tag', async () => {
assetMock.getAssetIdByCity.mockResolvedValue({
fieldName: 'exifInfo.city',
items: [{ value: 'Paris', data: assetStub.image.id }],
items: [{ value: 'test-city', data: assetStub.withLocation.id }],
});
assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]);
assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.withLocation]);
const expectedResponse = [
{ fieldName: 'exifInfo.city', items: [{ value: 'Paris', data: mapAsset(assetStub.image) }] },
{ fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] },
];
const result = await sut.getExploreData(authStub.user1);
+5 -12
View File
@@ -34,16 +34,10 @@ export class SearchService extends BaseService {
async getExploreData(auth: AuthDto): Promise<SearchExploreItem<AssetResponseDto>[]> {
const options = { maxFields: 12, minAssetsPerField: 5 };
const result = await this.assetRepository.getAssetIdByCity(auth.user.id, options);
const results = [result];
const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
const assets = await this.assetRepository.getByIdsWithAllRelations([...assetIds]);
const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)]));
return results.map(({ fieldName, items }) => ({
fieldName,
items: items.map(({ value, data }) => ({ value, data: assetMap.get(data) as AssetResponseDto })),
}));
const cities = await this.assetRepository.getAssetIdByCity(auth.user.id, options);
const assets = await this.assetRepository.getByIdsWithAllRelations(cities.items.map(({ data }) => data));
const items = assets.map((asset) => ({ value: asset.exifInfo!.city!, data: mapAsset(asset, { auth }) }));
return [{ fieldName: cities.fieldName, items }];
}
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
@@ -57,14 +51,13 @@ export class SearchService extends BaseService {
const page = dto.page ?? 1;
const size = dto.size || 250;
const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const;
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
...dto,
checksum,
userIds,
orderDirection: dto.order ? enumToOrder[dto.order] : 'DESC',
orderDirection: dto.order ?? AssetOrder.DESC,
},
);
+17 -11
View File
@@ -61,12 +61,15 @@ describe(TimelineService.name, () => {
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
userIds: [authStub.admin.user.id],
});
expect(assetMock.getTimeBucket).toHaveBeenCalledWith(
'bucket',
expect.objectContaining({
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
userIds: [authStub.admin.user.id],
}),
);
});
it('should include partner shared assets', async () => {
@@ -143,11 +146,14 @@ describe(TimelineService.name, () => {
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userIds: [authStub.admin.user.id],
});
expect(assetMock.getTimeBucket).toHaveBeenCalledWith(
'bucket',
expect.objectContaining({
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userIds: [authStub.admin.user.id],
}),
);
});
it('should throw an error if withParners is true and isArchived true or undefined', async () => {