mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-08 12:19:05 +00:00
refactor(server): jobs and processors (#1787)
* refactor: jobs and processors * refactor: storage migration processor * fix: tests * fix: code warning * chore: ignore coverage from infra * fix: sync move asset logic between job core and asset core * refactor: move error handling inside of catch * refactor(server): job core into dedicated service calls * refactor: smart info * fix: tests * chore: smart info tests * refactor: use asset repository * refactor: thumbnail processor * chore: coverage reqs
This commit is contained in:
3
server/libs/domain/src/smart-info/index.ts
Normal file
3
server/libs/domain/src/smart-info/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './machine-learning.interface';
|
||||
export * from './smart-info.repository';
|
||||
export * from './smart-info.service';
|
||||
@@ -0,0 +1,10 @@
|
||||
export const IMachineLearningRepository = 'IMachineLearningRepository';
|
||||
|
||||
export interface MachineLearningInput {
|
||||
thumbnailPath: string;
|
||||
}
|
||||
|
||||
export interface IMachineLearningRepository {
|
||||
tagImage(input: MachineLearningInput): Promise<string[]>;
|
||||
detectObjects(input: MachineLearningInput): Promise<string[]>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SmartInfoEntity } from '@app/infra/db/entities';
|
||||
|
||||
export const ISmartInfoRepository = 'ISmartInfoRepository';
|
||||
|
||||
export interface ISmartInfoRepository {
|
||||
upsert(info: Partial<SmartInfoEntity>): Promise<void>;
|
||||
}
|
||||
102
server/libs/domain/src/smart-info/smart-info.service.spec.ts
Normal file
102
server/libs/domain/src/smart-info/smart-info.service.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { AssetEntity } from '@app/infra/db/entities';
|
||||
import { newMachineLearningRepositoryMock, newSmartInfoRepositoryMock } from '../../test';
|
||||
import { IMachineLearningRepository } from './machine-learning.interface';
|
||||
import { ISmartInfoRepository } from './smart-info.repository';
|
||||
import { SmartInfoService } from './smart-info.service';
|
||||
|
||||
const asset = {
|
||||
id: 'asset-1',
|
||||
resizePath: 'path/to/resize.ext',
|
||||
} as AssetEntity;
|
||||
|
||||
describe(SmartInfoService.name, () => {
|
||||
let sut: SmartInfoService;
|
||||
let smartMock: jest.Mocked<ISmartInfoRepository>;
|
||||
let machineMock: jest.Mocked<IMachineLearningRepository>;
|
||||
|
||||
beforeEach(async () => {
|
||||
smartMock = newSmartInfoRepositoryMock();
|
||||
machineMock = newMachineLearningRepositoryMock();
|
||||
sut = new SmartInfoService(smartMock, machineMock);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('handleTagImage', () => {
|
||||
it('should skip assets without a resize path', async () => {
|
||||
await sut.handleTagImage({ asset: { resizePath: '' } as AssetEntity });
|
||||
|
||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
||||
expect(machineMock.tagImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save the returned tags', async () => {
|
||||
machineMock.tagImage.mockResolvedValue(['tag1', 'tag2', 'tag3']);
|
||||
|
||||
await sut.handleTagImage({ asset });
|
||||
|
||||
expect(machineMock.tagImage).toHaveBeenCalledWith({ thumbnailPath: 'path/to/resize.ext' });
|
||||
expect(smartMock.upsert).toHaveBeenCalledWith({
|
||||
assetId: 'asset-1',
|
||||
tags: ['tag1', 'tag2', 'tag3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an error with the machine learning pipeline', async () => {
|
||||
machineMock.tagImage.mockRejectedValue(new Error('Unable to read thumbnail'));
|
||||
|
||||
await sut.handleTagImage({ asset });
|
||||
|
||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should no update the smart info if no tags were returned', async () => {
|
||||
machineMock.tagImage.mockResolvedValue([]);
|
||||
|
||||
await sut.handleTagImage({ asset });
|
||||
|
||||
expect(machineMock.tagImage).toHaveBeenCalled();
|
||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDetectObjects', () => {
|
||||
it('should skip assets without a resize path', async () => {
|
||||
await sut.handleDetectObjects({ asset: { resizePath: '' } as AssetEntity });
|
||||
|
||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
||||
expect(machineMock.detectObjects).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save the returned objects', async () => {
|
||||
machineMock.detectObjects.mockResolvedValue(['obj1', 'obj2', 'obj3']);
|
||||
|
||||
await sut.handleDetectObjects({ asset });
|
||||
|
||||
expect(machineMock.detectObjects).toHaveBeenCalledWith({ thumbnailPath: 'path/to/resize.ext' });
|
||||
expect(smartMock.upsert).toHaveBeenCalledWith({
|
||||
assetId: 'asset-1',
|
||||
objects: ['obj1', 'obj2', 'obj3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an error with the machine learning pipeline', async () => {
|
||||
machineMock.detectObjects.mockRejectedValue(new Error('Unable to read thumbnail'));
|
||||
|
||||
await sut.handleDetectObjects({ asset });
|
||||
|
||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should no update the smart info if no objects were returned', async () => {
|
||||
machineMock.detectObjects.mockResolvedValue([]);
|
||||
|
||||
await sut.handleDetectObjects({ asset });
|
||||
|
||||
expect(machineMock.detectObjects).toHaveBeenCalled();
|
||||
expect(smartMock.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
49
server/libs/domain/src/smart-info/smart-info.service.ts
Normal file
49
server/libs/domain/src/smart-info/smart-info.service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { MACHINE_LEARNING_ENABLED } from '@app/common';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { IAssetJob } from '../job';
|
||||
import { IMachineLearningRepository } from './machine-learning.interface';
|
||||
import { ISmartInfoRepository } from './smart-info.repository';
|
||||
|
||||
@Injectable()
|
||||
export class SmartInfoService {
|
||||
private logger = new Logger(SmartInfoService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(ISmartInfoRepository) private repository: ISmartInfoRepository,
|
||||
@Inject(IMachineLearningRepository) private machineLearning: IMachineLearningRepository,
|
||||
) {}
|
||||
|
||||
async handleTagImage(data: IAssetJob) {
|
||||
const { asset } = data;
|
||||
|
||||
if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = await this.machineLearning.tagImage({ thumbnailPath: asset.resizePath });
|
||||
if (tags.length > 0) {
|
||||
await this.repository.upsert({ assetId: asset.id, tags });
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable to run image tagging pipeline: ${asset.id}`, error?.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async handleDetectObjects(data: IAssetJob) {
|
||||
const { asset } = data;
|
||||
|
||||
if (!MACHINE_LEARNING_ENABLED || !asset.resizePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const objects = await this.machineLearning.detectObjects({ thumbnailPath: asset.resizePath });
|
||||
if (objects.length > 0) {
|
||||
await this.repository.upsert({ assetId: asset.id, objects });
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Unable run object detection pipeline: ${asset.id}`, error?.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user