mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
chore(server): introduce proper job status (#7932)
* introduce proper job status * fix condition for onDone jobs * fix tests
This commit is contained in:
parent
07e8f79563
commit
a46366d336
@ -22,6 +22,7 @@ import {
|
|||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
JobItem,
|
JobItem,
|
||||||
|
JobStatus,
|
||||||
TimeBucketOptions,
|
TimeBucketOptions,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { StorageCore, StorageFolder } from '../storage';
|
import { StorageCore, StorageFolder } from '../storage';
|
||||||
@ -384,7 +385,7 @@ export class AssetService {
|
|||||||
this.communicationRepository.send(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids);
|
this.communicationRepository.send(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetDeletionCheck() {
|
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig();
|
||||||
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
||||||
const trashedBefore = DateTime.now()
|
const trashedBefore = DateTime.now()
|
||||||
@ -400,10 +401,10 @@ export class AssetService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetDeletion(job: IAssetDeletionJob) {
|
async handleAssetDeletion(job: IAssetDeletionJob): Promise<JobStatus> {
|
||||||
const { id, fromExternal } = job;
|
const { id, fromExternal } = job;
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetRepository.getById(id, {
|
||||||
@ -416,12 +417,12 @@ export class AssetService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore requests that are not from external library job but is for an external asset
|
// Ignore requests that are not from external library job but is for an external asset
|
||||||
if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) {
|
if (!fromExternal && (!asset.library || asset.library.type === LibraryType.EXTERNAL)) {
|
||||||
return false;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the parent of the stack children with a new asset
|
// Replace the parent of the stack children with a new asset
|
||||||
@ -456,7 +457,7 @@ export class AssetService {
|
|||||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
|
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
|
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
|
JobStatus,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { AuditService } from './audit.service';
|
import { AuditService } from './audit.service';
|
||||||
|
|
||||||
@ -48,8 +49,8 @@ describe(AuditService.name, () => {
|
|||||||
|
|
||||||
describe('handleCleanup', () => {
|
describe('handleCleanup', () => {
|
||||||
it('should delete old audit entries', async () => {
|
it('should delete old audit entries', async () => {
|
||||||
await expect(sut.handleCleanup()).resolves.toBe(true);
|
await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(auditMock.removeBefore).toBeCalledWith(expect.any(Date));
|
expect(auditMock.removeBefore).toHaveBeenCalledWith(expect.any(Date));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
|
JobStatus,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { StorageCore, StorageFolder } from '../storage';
|
import { StorageCore, StorageFolder } from '../storage';
|
||||||
import {
|
import {
|
||||||
@ -44,9 +45,9 @@ export class AuditService {
|
|||||||
this.access = AccessCore.create(accessRepository);
|
this.access = AccessCore.create(accessRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCleanup(): Promise<boolean> {
|
async handleCleanup(): Promise<JobStatus> {
|
||||||
await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
await this.repository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise<AuditDeletesResponseDto> {
|
||||||
|
@ -16,13 +16,14 @@ import {
|
|||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
JobHandler,
|
JobHandler,
|
||||||
JobItem,
|
JobItem,
|
||||||
|
JobStatus,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
||||||
import { JobCommand, JobName, QueueName } from './job.constants';
|
import { JobCommand, JobName, QueueName } from './job.constants';
|
||||||
import { JobService } from './job.service';
|
import { JobService } from './job.service';
|
||||||
|
|
||||||
const makeMockHandlers = (success: boolean) => {
|
const makeMockHandlers = (status: JobStatus) => {
|
||||||
const mock = jest.fn().mockResolvedValue(success);
|
const mock = jest.fn().mockResolvedValue(status);
|
||||||
return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record<
|
return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record<
|
||||||
JobName,
|
JobName,
|
||||||
JobHandler
|
JobHandler
|
||||||
@ -221,13 +222,13 @@ describe(JobService.name, () => {
|
|||||||
|
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
it('should register a handler for each queue', async () => {
|
it('should register a handler for each queue', async () => {
|
||||||
await sut.init(makeMockHandlers(true));
|
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
|
||||||
expect(configMock.load).toHaveBeenCalled();
|
expect(configMock.load).toHaveBeenCalled();
|
||||||
expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
|
expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should subscribe to config changes', async () => {
|
it('should subscribe to config changes', async () => {
|
||||||
await sut.init(makeMockHandlers(false));
|
await sut.init(makeMockHandlers(JobStatus.FAILED));
|
||||||
|
|
||||||
SystemConfigCore.create(newSystemConfigRepositoryMock(false)).config$.next({
|
SystemConfigCore.create(newSystemConfigRepositoryMock(false)).config$.next({
|
||||||
job: {
|
job: {
|
||||||
@ -332,7 +333,7 @@ describe(JobService.name, () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await sut.init(makeMockHandlers(true));
|
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
|
||||||
await jobMock.addHandler.mock.calls[0][2](item);
|
await jobMock.addHandler.mock.calls[0][2](item);
|
||||||
|
|
||||||
if (jobs.length > 1) {
|
if (jobs.length > 1) {
|
||||||
@ -348,7 +349,7 @@ describe(JobService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should not queue any jobs when ${item.name} finishes with 'false'`, async () => {
|
it(`should not queue any jobs when ${item.name} finishes with 'false'`, async () => {
|
||||||
await sut.init(makeMockHandlers(false));
|
await sut.init(makeMockHandlers(JobStatus.FAILED));
|
||||||
await jobMock.addHandler.mock.calls[0][2](item);
|
await jobMock.addHandler.mock.calls[0][2](item);
|
||||||
|
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
JobHandler,
|
JobHandler,
|
||||||
JobItem,
|
JobItem,
|
||||||
|
JobStatus,
|
||||||
QueueCleanType,
|
QueueCleanType,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
import { FeatureFlag, SystemConfigCore } from '../system-config/system-config.core';
|
||||||
@ -155,8 +156,8 @@ export class JobService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const handler = jobHandlers[name];
|
const handler = jobHandlers[name];
|
||||||
const success = await handler(data);
|
const status = await handler(data);
|
||||||
if (success) {
|
if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) {
|
||||||
await this.onDone(item);
|
await this.onDone(item);
|
||||||
}
|
}
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
ILibraryRepository,
|
ILibraryRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
|
JobStatus,
|
||||||
StorageEventType,
|
StorageEventType,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { SystemConfigCore } from '../system-config/system-config.core';
|
import { SystemConfigCore } from '../system-config/system-config.core';
|
||||||
@ -214,7 +215,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
libraryMock.get.mockResolvedValue(libraryStub.uploadLibrary1);
|
||||||
|
|
||||||
await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(false);
|
await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore import paths that do not exist', async () => {
|
it('should ignore import paths that do not exist', async () => {
|
||||||
@ -340,7 +341,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.create.mock.calls).toEqual([
|
expect(assetMock.create.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
@ -388,7 +389,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.create.mock.calls).toEqual([
|
expect(assetMock.create.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
@ -435,7 +436,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
||||||
assetMock.create.mockResolvedValue(assetStub.video);
|
assetMock.create.mockResolvedValue(assetStub.video);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.create.mock.calls).toEqual([
|
expect(assetMock.create.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
@ -491,7 +492,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
|
libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() });
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(false);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED);
|
||||||
|
|
||||||
expect(assetMock.create.mock.calls).toEqual([]);
|
expect(assetMock.create.mock.calls).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -512,7 +513,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
||||||
|
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
@ -529,7 +530,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
@ -560,7 +561,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
|
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true });
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
@ -578,7 +579,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline);
|
||||||
assetMock.create.mockResolvedValue(assetStub.offline);
|
assetMock.create.mockResolvedValue(assetStub.offline);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
|
expect(assetMock.save).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false });
|
||||||
|
|
||||||
@ -611,7 +612,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should refresh an existing asset if forced', async () => {
|
it('should refresh an existing asset if forced', async () => {
|
||||||
@ -625,7 +626,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], {
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], {
|
||||||
fileCreatedAt: new Date('2023-01-01'),
|
fileCreatedAt: new Date('2023-01-01'),
|
||||||
@ -653,7 +654,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(true);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.create).toHaveBeenCalled();
|
expect(assetMock.create).toHaveBeenCalled();
|
||||||
const createdAsset = assetMock.create.mock.calls[0][0];
|
const createdAsset = assetMock.create.mock.calls[0][0];
|
||||||
@ -1076,7 +1077,7 @@ describe(LibraryService.name, () => {
|
|||||||
describe('handleQueueCleanup', () => {
|
describe('handleQueueCleanup', () => {
|
||||||
it('should queue cleanup jobs', async () => {
|
it('should queue cleanup jobs', async () => {
|
||||||
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]);
|
libraryMock.getAllDeleted.mockResolvedValue([libraryStub.uploadLibrary1, libraryStub.externalLibrary1]);
|
||||||
await expect(sut.handleQueueCleanup()).resolves.toBe(true);
|
await expect(sut.handleQueueCleanup()).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.uploadLibrary1.id } },
|
{ name: JobName.LIBRARY_DELETE, data: { id: libraryStub.uploadLibrary1.id } },
|
||||||
@ -1363,7 +1364,7 @@ describe(LibraryService.name, () => {
|
|||||||
libraryMock.getAssetIds.mockResolvedValue([]);
|
libraryMock.getAssetIds.mockResolvedValue([]);
|
||||||
libraryMock.delete.mockImplementation(async () => {});
|
libraryMock.delete.mockImplementation(async () => {});
|
||||||
|
|
||||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(false);
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete an empty library', async () => {
|
it('should delete an empty library', async () => {
|
||||||
@ -1371,7 +1372,7 @@ describe(LibraryService.name, () => {
|
|||||||
libraryMock.getAssetIds.mockResolvedValue([]);
|
libraryMock.getAssetIds.mockResolvedValue([]);
|
||||||
libraryMock.delete.mockImplementation(async () => {});
|
libraryMock.delete.mockImplementation(async () => {});
|
||||||
|
|
||||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(true);
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a library with assets', async () => {
|
it('should delete a library with assets', async () => {
|
||||||
@ -1381,7 +1382,7 @@ describe(LibraryService.name, () => {
|
|||||||
|
|
||||||
assetMock.getById.mockResolvedValue(assetStub.image1);
|
assetMock.getById.mockResolvedValue(assetStub.image1);
|
||||||
|
|
||||||
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(true);
|
await expect(sut.handleDeleteLibrary({ id: libraryStub.uploadLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1475,7 +1476,7 @@ describe(LibraryService.name, () => {
|
|||||||
it('should queue the refresh job', async () => {
|
it('should queue the refresh job', async () => {
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
||||||
|
|
||||||
await expect(sut.handleQueueAllScan({})).resolves.toBe(true);
|
await expect(sut.handleQueueAllScan({})).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(jobMock.queue.mock.calls).toEqual([
|
expect(jobMock.queue.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
@ -1500,7 +1501,7 @@ describe(LibraryService.name, () => {
|
|||||||
it('should queue the force refresh job', async () => {
|
it('should queue the force refresh job', async () => {
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
|
||||||
|
|
||||||
await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(true);
|
await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.LIBRARY_QUEUE_CLEANUP,
|
name: JobName.LIBRARY_QUEUE_CLEANUP,
|
||||||
@ -1525,7 +1526,7 @@ describe(LibraryService.name, () => {
|
|||||||
assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
|
assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false });
|
||||||
assetMock.getById.mockResolvedValue(assetStub.image1);
|
assetMock.getById.mockResolvedValue(assetStub.image1);
|
||||||
|
|
||||||
await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(true);
|
await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
ILibraryRepository,
|
ILibraryRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
|
JobStatus,
|
||||||
StorageEventType,
|
StorageEventType,
|
||||||
WithProperty,
|
WithProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
@ -241,13 +242,13 @@ export class LibraryService extends EventEmitter {
|
|||||||
return libraries.map((library) => mapLibrary(library));
|
return libraries.map((library) => mapLibrary(library));
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueCleanup(): Promise<boolean> {
|
async handleQueueCleanup(): Promise<JobStatus> {
|
||||||
this.logger.debug('Cleaning up any pending library deletions');
|
this.logger.debug('Cleaning up any pending library deletions');
|
||||||
const pendingDeletion = await this.repository.getAllDeleted();
|
const pendingDeletion = await this.repository.getAllDeleted();
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
|
pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
|
||||||
);
|
);
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
||||||
@ -410,10 +411,10 @@ export class LibraryService extends EventEmitter {
|
|||||||
await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } });
|
await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteLibrary(job: IEntityJob): Promise<boolean> {
|
async handleDeleteLibrary(job: IEntityJob): Promise<JobStatus> {
|
||||||
const library = await this.repository.get(job.id, true);
|
const library = await this.repository.get(job.id, true);
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO use pagination
|
// TODO use pagination
|
||||||
@ -427,10 +428,10 @@ export class LibraryService extends EventEmitter {
|
|||||||
this.logger.log(`Deleting library ${job.id}`);
|
this.logger.log(`Deleting library ${job.id}`);
|
||||||
await this.repository.delete(job.id);
|
await this.repository.delete(job.id);
|
||||||
}
|
}
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetRefresh(job: ILibraryFileJob) {
|
async handleAssetRefresh(job: ILibraryFileJob): Promise<JobStatus> {
|
||||||
const assetPath = path.normalize(job.assetPath);
|
const assetPath = path.normalize(job.assetPath);
|
||||||
|
|
||||||
const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath);
|
const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath);
|
||||||
@ -445,7 +446,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
this.logger.debug(`Marking asset as offline: ${assetPath}`);
|
this.logger.debug(`Marking asset as offline: ${assetPath}`);
|
||||||
|
|
||||||
await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: true });
|
await this.assetRepository.save({ id: existingAssetEntity.id, isOffline: true });
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
} else {
|
} else {
|
||||||
// File can't be accessed and does not already exist in db
|
// File can't be accessed and does not already exist in db
|
||||||
throw new BadRequestException('Cannot access file', { cause: error });
|
throw new BadRequestException('Cannot access file', { cause: error });
|
||||||
@ -483,7 +484,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
|
|
||||||
if (!doImport && !doRefresh) {
|
if (!doImport && !doRefresh) {
|
||||||
// If we don't import, exit here
|
// If we don't import, exit here
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
let assetType: AssetType;
|
let assetType: AssetType;
|
||||||
@ -509,7 +510,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
const library = await this.repository.get(job.id, true);
|
const library = await this.repository.get(job.id, true);
|
||||||
if (library?.deletedAt) {
|
if (library?.deletedAt) {
|
||||||
this.logger.error('Cannot import asset into deleted library');
|
this.logger.error('Cannot import asset into deleted library');
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
|
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
|
||||||
@ -540,7 +541,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Not importing and not refreshing, do nothing
|
// Not importing and not refreshing, do nothing
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Queuing metadata extraction for: ${assetPath}`);
|
this.logger.debug(`Queuing metadata extraction for: ${assetPath}`);
|
||||||
@ -551,7 +552,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: assetId } });
|
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: assetId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) {
|
async queueScan(auth: AuthDto, id: string, dto: ScanLibraryDto) {
|
||||||
@ -584,7 +585,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueAllScan(job: IBaseJob): Promise<boolean> {
|
async handleQueueAllScan(job: IBaseJob): Promise<JobStatus> {
|
||||||
this.logger.debug(`Refreshing all external libraries: force=${job.force}`);
|
this.logger.debug(`Refreshing all external libraries: force=${job.force}`);
|
||||||
|
|
||||||
// Queue cleanup
|
// Queue cleanup
|
||||||
@ -602,10 +603,10 @@ export class LibraryService extends EventEmitter {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleOfflineRemoval(job: IEntityJob): Promise<boolean> {
|
async handleOfflineRemoval(job: IEntityJob): Promise<JobStatus> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id),
|
this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id),
|
||||||
);
|
);
|
||||||
@ -617,14 +618,14 @@ export class LibraryService extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise<boolean> {
|
async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise<JobStatus> {
|
||||||
const library = await this.repository.get(job.id);
|
const library = await this.repository.get(job.id);
|
||||||
if (!library || library.type !== LibraryType.EXTERNAL) {
|
if (!library || library.type !== LibraryType.EXTERNAL) {
|
||||||
this.logger.warn('Can only refresh external libraries');
|
this.logger.warn('Can only refresh external libraries');
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Refreshing library: ${job.id}`);
|
this.logger.log(`Refreshing library: ${job.id}`);
|
||||||
@ -694,7 +695,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
|
|
||||||
await this.repository.update({ id: job.id, refreshedAt: new Date() });
|
await this.repository.update({ id: job.id, refreshedAt: new Date() });
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPathTrie(library: LibraryEntity): Promise<Trie<string>> {
|
private async getPathTrie(library: LibraryEntity): Promise<Trie<string>> {
|
||||||
|
@ -34,6 +34,7 @@ import {
|
|||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
|
JobStatus,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { MediaService } from './media.service';
|
import { MediaService } from './media.service';
|
||||||
@ -1214,22 +1215,22 @@ describe(MediaService.name, () => {
|
|||||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if hwaccel is enabled for an unsupported codec', async () => {
|
it('should fail if hwaccel is enabled for an unsupported codec', async () => {
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
configMock.load.mockResolvedValue([
|
configMock.load.mockResolvedValue([
|
||||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.VP9 },
|
||||||
]);
|
]);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false);
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if hwaccel option is invalid', async () => {
|
it('should fail if hwaccel option is invalid', async () => {
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: 'invalid' }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: 'invalid' }]);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false);
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1548,12 +1549,12 @@ describe(MediaService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for qsv if no hw devices', async () => {
|
it('should fail for qsv if no hw devices', async () => {
|
||||||
storageMock.readdir.mockResolvedValue([]);
|
storageMock.readdir.mockResolvedValue([]);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.QSV }]);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false);
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1777,12 +1778,12 @@ describe(MediaService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for vaapi if no hw devices', async () => {
|
it('should fail for vaapi if no hw devices', async () => {
|
||||||
storageMock.readdir.mockResolvedValue([]);
|
storageMock.readdir.mockResolvedValue([]);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.VAAPI }]);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toEqual(false);
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
expect(mediaMock.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
JobItem,
|
JobItem,
|
||||||
|
JobStatus,
|
||||||
VideoCodecHWConfig,
|
VideoCodecHWConfig,
|
||||||
VideoStreamInfo,
|
VideoStreamInfo,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
@ -70,7 +71,7 @@ export class MediaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueGenerateThumbnails({ force }: IBaseJob) {
|
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination)
|
? this.assetRepository.getAll(pagination)
|
||||||
@ -118,10 +119,10 @@ export class MediaService {
|
|||||||
|
|
||||||
await this.jobRepository.queueAll(jobs);
|
await this.jobRepository.queueAll(jobs);
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueMigration() {
|
async handleQueueMigration(): Promise<JobStatus> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
this.assetRepository.getAll(pagination),
|
this.assetRepository.getAll(pagination),
|
||||||
);
|
);
|
||||||
@ -148,31 +149,31 @@ export class MediaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetMigration({ id }: IEntityJob) {
|
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL);
|
await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL);
|
||||||
await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL);
|
await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL);
|
||||||
await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO);
|
await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO);
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGenerateJpegThumbnail({ id }: IEntityJob) {
|
async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resizePath = await this.generateThumbnail(asset, 'jpeg');
|
const resizePath = await this.generateThumbnail(asset, 'jpeg');
|
||||||
await this.assetRepository.save({ id: asset.id, resizePath });
|
await this.assetRepository.save({ id: asset.id, resizePath });
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') {
|
private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') {
|
||||||
@ -214,30 +215,30 @@ export class MediaService {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGenerateWebpThumbnail({ id }: IEntityJob) {
|
async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const webpPath = await this.generateThumbnail(asset, 'webp');
|
const webpPath = await this.generateThumbnail(asset, 'webp');
|
||||||
await this.assetRepository.save({ id: asset.id, webpPath });
|
await this.assetRepository.save({ id: asset.id, webpPath });
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise<boolean> {
|
async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset?.resizePath) {
|
if (!asset?.resizePath) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath);
|
const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath);
|
||||||
await this.assetRepository.save({ id: asset.id, thumbhash });
|
await this.assetRepository.save({ id: asset.id, thumbhash });
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueVideoConversion(job: IBaseJob) {
|
async handleQueueVideoConversion(job: IBaseJob): Promise<JobStatus> {
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
@ -252,13 +253,13 @@ export class MediaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleVideoConversion({ id }: IEntityJob) {
|
async handleVideoConversion({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset || asset.type !== AssetType.VIDEO) {
|
if (!asset || asset.type !== AssetType.VIDEO) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = asset.originalPath;
|
const input = asset.originalPath;
|
||||||
@ -270,12 +271,12 @@ export class MediaService {
|
|||||||
const mainAudioStream = this.getMainStream(audioStreams);
|
const mainAudioStream = this.getMainStream(audioStreams);
|
||||||
const containerExtension = format.formatName;
|
const containerExtension = format.formatName;
|
||||||
if (!mainVideoStream || !containerExtension) {
|
if (!mainVideoStream || !containerExtension) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mainVideoStream.height || !mainVideoStream.width) {
|
if (!mainVideoStream.height || !mainVideoStream.width) {
|
||||||
this.logger.warn(`Skipped transcoding for asset ${asset.id}: no video streams found`);
|
this.logger.warn(`Skipped transcoding for asset ${asset.id}: no video streams found`);
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ffmpeg: config } = await this.configCore.getConfig();
|
const { ffmpeg: config } = await this.configCore.getConfig();
|
||||||
@ -288,7 +289,7 @@ export class MediaService {
|
|||||||
await this.assetRepository.save({ id: asset.id, encodedVideoPath: null });
|
await this.assetRepository.save({ id: asset.id, encodedVideoPath: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transcodeOptions;
|
let transcodeOptions;
|
||||||
@ -298,7 +299,7 @@ export class MediaService {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
|
this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
|
this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
|
||||||
@ -322,7 +323,7 @@ export class MediaService {
|
|||||||
|
|
||||||
await this.assetRepository.save({ id: asset.id, encodedVideoPath: output });
|
await this.assetRepository.save({ id: asset.id, encodedVideoPath: output });
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMainStream<T extends VideoStreamInfo | AudioStreamInfo>(streams: T[]): T {
|
private getMainStream<T extends VideoStreamInfo | AudioStreamInfo>(streams: T[]): T {
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
ImmichTags,
|
ImmichTags,
|
||||||
|
JobStatus,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { MetadataService, Orientation } from './metadata.service';
|
import { MetadataService, Orientation } from './metadata.service';
|
||||||
@ -113,7 +114,7 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
describe('handleLivePhotoLinking', () => {
|
describe('handleLivePhotoLinking', () => {
|
||||||
it('should handle an asset that could not be found', async () => {
|
it('should handle an asset that could not be found', async () => {
|
||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(false);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
@ -123,7 +124,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should handle an asset without exif info', async () => {
|
it('should handle an asset without exif info', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]);
|
||||||
|
|
||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(false);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
@ -133,7 +134,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should handle livePhotoCID not set', async () => {
|
it('should handle livePhotoCID not set', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]);
|
||||||
|
|
||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(true);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
expect(assetMock.findLivePhotoMatch).not.toHaveBeenCalled();
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
@ -148,7 +149,9 @@ describe(MetadataService.name, () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(true);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe(
|
||||||
|
JobStatus.SKIPPED,
|
||||||
|
);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
|
expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||||
livePhotoCID: assetStub.livePhotoStillAsset.id,
|
livePhotoCID: assetStub.livePhotoStillAsset.id,
|
||||||
@ -169,7 +172,9 @@ describe(MetadataService.name, () => {
|
|||||||
]);
|
]);
|
||||||
assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
|
|
||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||||
|
JobStatus.SUCCESS,
|
||||||
|
);
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
||||||
expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
|
expect(assetMock.findLivePhotoMatch).toHaveBeenCalledWith({
|
||||||
livePhotoCID: assetStub.livePhotoMotionAsset.id,
|
livePhotoCID: assetStub.livePhotoMotionAsset.id,
|
||||||
@ -194,7 +199,9 @@ describe(MetadataService.name, () => {
|
|||||||
]);
|
]);
|
||||||
assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
assetMock.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||||
|
|
||||||
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true);
|
await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||||
|
JobStatus.SUCCESS,
|
||||||
|
);
|
||||||
expect(communicationMock.send).toHaveBeenCalledWith(
|
expect(communicationMock.send).toHaveBeenCalledWith(
|
||||||
ClientEvent.ASSET_HIDDEN,
|
ClientEvent.ASSET_HIDDEN,
|
||||||
assetStub.livePhotoMotionAsset.ownerId,
|
assetStub.livePhotoMotionAsset.ownerId,
|
||||||
@ -207,7 +214,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should queue metadata extraction for all assets without exif values', async () => {
|
it('should queue metadata extraction for all assets without exif values', async () => {
|
||||||
assetMock.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
assetMock.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
||||||
|
|
||||||
await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(true);
|
await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(assetMock.getWithout).toHaveBeenCalled();
|
expect(assetMock.getWithout).toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
@ -220,7 +227,7 @@ describe(MetadataService.name, () => {
|
|||||||
it('should queue metadata extraction for all assets', async () => {
|
it('should queue metadata extraction for all assets', async () => {
|
||||||
assetMock.getAll.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
assetMock.getAll.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
||||||
|
|
||||||
await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(true);
|
await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(assetMock.getAll).toHaveBeenCalled();
|
expect(assetMock.getAll).toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
@ -237,7 +244,7 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an asset that could not be found', async () => {
|
it('should handle an asset that could not be found', async () => {
|
||||||
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(false);
|
await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
expect(assetMock.upsertExif).not.toHaveBeenCalled();
|
expect(assetMock.upsertExif).not.toHaveBeenCalled();
|
||||||
@ -630,19 +637,13 @@ describe(MetadataService.name, () => {
|
|||||||
describe('handleSidecarSync', () => {
|
describe('handleSidecarSync', () => {
|
||||||
it('should do nothing if asset could not be found', async () => {
|
it('should do nothing if asset could not be found', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false);
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if asset has no sidecar path', async () => {
|
it('should do nothing if asset has no sidecar path', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false);
|
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing if asset has no sidecar path', async () => {
|
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.image.id })).resolves.toBe(false);
|
|
||||||
expect(assetMock.save).not.toHaveBeenCalled();
|
expect(assetMock.save).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -650,7 +651,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
storageMock.checkFileExists.mockResolvedValue(true);
|
storageMock.checkFileExists.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
id: assetStub.sidecar.id,
|
id: assetStub.sidecar.id,
|
||||||
@ -663,7 +664,7 @@ describe(MetadataService.name, () => {
|
|||||||
storageMock.checkFileExists.mockResolvedValueOnce(false);
|
storageMock.checkFileExists.mockResolvedValueOnce(false);
|
||||||
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(true);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecarWithoutExt.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
assetStub.sidecarWithoutExt.sidecarPath,
|
assetStub.sidecarWithoutExt.sidecarPath,
|
||||||
@ -680,7 +681,7 @@ describe(MetadataService.name, () => {
|
|||||||
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||||
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
storageMock.checkFileExists.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(1, assetStub.sidecar.sidecarPath, constants.R_OK);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
expect(storageMock.checkFileExists).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
@ -697,7 +698,7 @@ describe(MetadataService.name, () => {
|
|||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
storageMock.checkFileExists.mockResolvedValue(false);
|
storageMock.checkFileExists.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(true);
|
await expect(sut.handleSidecarSync({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
expect(storageMock.checkFileExists).toHaveBeenCalledWith(`${assetStub.sidecar.originalPath}.xmp`, constants.R_OK);
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
id: assetStub.sidecar.id,
|
id: assetStub.sidecar.id,
|
||||||
@ -754,13 +755,13 @@ describe(MetadataService.name, () => {
|
|||||||
describe('handleSidecarWrite', () => {
|
describe('handleSidecarWrite', () => {
|
||||||
it('should skip assets that do not exist anymore', async () => {
|
it('should skip assets that do not exist anymore', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([]);
|
assetMock.getByIds.mockResolvedValue([]);
|
||||||
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(false);
|
await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.FAILED);
|
||||||
expect(metadataMock.writeTags).not.toHaveBeenCalled();
|
expect(metadataMock.writeTags).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip jobs with not metadata', async () => {
|
it('should skip jobs with not metadata', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
assetMock.getByIds.mockResolvedValue([assetStub.sidecar]);
|
||||||
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(true);
|
await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(metadataMock.writeTags).not.toHaveBeenCalled();
|
expect(metadataMock.writeTags).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -778,7 +779,7 @@ describe(MetadataService.name, () => {
|
|||||||
longitude: gps,
|
longitude: gps,
|
||||||
dateTimeOriginal: date,
|
dateTimeOriginal: date,
|
||||||
}),
|
}),
|
||||||
).resolves.toBe(true);
|
).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(metadataMock.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, {
|
expect(metadataMock.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, {
|
||||||
ImageDescription: description,
|
ImageDescription: description,
|
||||||
CreationDate: date,
|
CreationDate: date,
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
ImmichTags,
|
ImmichTags,
|
||||||
|
JobStatus,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { StorageCore } from '../storage';
|
import { StorageCore } from '../storage';
|
||||||
@ -151,15 +152,15 @@ export class MetadataService {
|
|||||||
await this.repository.teardown();
|
await this.repository.teardown();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLivePhotoLinking(job: IEntityJob) {
|
async handleLivePhotoLinking(job: IEntityJob): Promise<JobStatus> {
|
||||||
const { id } = job;
|
const { id } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
||||||
if (!asset?.exifInfo) {
|
if (!asset?.exifInfo) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!asset.exifInfo.livePhotoCID) {
|
if (!asset.exifInfo.livePhotoCID) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO;
|
const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO;
|
||||||
@ -171,7 +172,7 @@ export class MetadataService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
|
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
|
||||||
@ -183,10 +184,10 @@ export class MetadataService {
|
|||||||
// Notify clients to hide the linked live photo asset
|
// Notify clients to hide the linked live photo asset
|
||||||
this.communicationRepository.send(ClientEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id);
|
this.communicationRepository.send(ClientEvent.ASSET_HIDDEN, motionAsset.ownerId, motionAsset.id);
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueMetadataExtraction(job: IBaseJob) {
|
async handleQueueMetadataExtraction(job: IBaseJob): Promise<JobStatus> {
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
@ -200,13 +201,13 @@ export class MetadataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleMetadataExtraction({ id }: IEntityJob) {
|
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { exifData, tags } = await this.exifData(asset);
|
const { exifData, tags } = await this.exifData(asset);
|
||||||
@ -260,10 +261,10 @@ export class MetadataService {
|
|||||||
metadataExtractedAt: new Date(),
|
metadataExtractedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueSidecar(job: IBaseJob) {
|
async handleQueueSidecar(job: IBaseJob): Promise<JobStatus> {
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
@ -280,22 +281,22 @@ export class MetadataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSidecarSync({ id }: IEntityJob) {
|
handleSidecarSync({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
return this.processSidecar(id, true);
|
return this.processSidecar(id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSidecarDiscovery({ id }: IEntityJob) {
|
handleSidecarDiscovery({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
return this.processSidecar(id, false);
|
return this.processSidecar(id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSidecarWrite(job: ISidecarWriteJob) {
|
async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude } = job;
|
const { id, description, dateTimeOriginal, latitude, longitude } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`;
|
const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`;
|
||||||
@ -310,7 +311,7 @@ export class MetadataService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (Object.keys(exif).length === 0) {
|
if (Object.keys(exif).length === 0) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.repository.writeTags(sidecarPath, exif);
|
await this.repository.writeTags(sidecarPath, exif);
|
||||||
@ -319,7 +320,7 @@ export class MetadataService {
|
|||||||
await this.assetRepository.save({ id, sidecarPath });
|
await this.assetRepository.save({ id, sidecarPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
||||||
@ -552,19 +553,19 @@ export class MetadataService {
|
|||||||
return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS');
|
return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processSidecar(id: string, isSync: boolean) {
|
private async processSidecar(id: string, isSync: boolean): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSync && !asset.sidecarPath) {
|
if (isSync && !asset.sidecarPath) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSync && (!asset.isVisible || asset.sidecarPath)) {
|
if (!isSync && (!asset.isVisible || asset.sidecarPath)) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||||
@ -587,11 +588,11 @@ export class MetadataService {
|
|||||||
|
|
||||||
if (sidecarPath) {
|
if (sidecarPath) {
|
||||||
await this.assetRepository.save({ id: asset.id, sidecarPath });
|
await this.assetRepository.save({ id: asset.id, sidecarPath });
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSync) {
|
if (!isSync) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@ -599,6 +600,6 @@ export class MetadataService {
|
|||||||
);
|
);
|
||||||
await this.assetRepository.save({ id: asset.id, sidecarPath: null });
|
await this.assetRepository.save({ id: asset.id, sidecarPath: null });
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import {
|
|||||||
ISearchRepository,
|
ISearchRepository,
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
|
JobStatus,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { PersonResponseDto, mapFaces, mapPerson } from './person.dto';
|
import { PersonResponseDto, mapFaces, mapPerson } from './person.dto';
|
||||||
@ -357,7 +358,7 @@ describe(PersonService.name, () => {
|
|||||||
describe('handlePersonMigration', () => {
|
describe('handlePersonMigration', () => {
|
||||||
it('should not move person files', async () => {
|
it('should not move person files', async () => {
|
||||||
personMock.getById.mockResolvedValue(null);
|
personMock.getById.mockResolvedValue(null);
|
||||||
await expect(sut.handlePersonMigration(personStub.noName)).resolves.toStrictEqual(false);
|
await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.FAILED);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -454,10 +455,10 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueDetectFaces', () => {
|
describe('handleQueueDetectFaces', () => {
|
||||||
it('should return if machine learning is disabled', async () => {
|
it('should skip if machine learning is disabled', async () => {
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||||
|
|
||||||
await expect(sut.handleQueueDetectFaces({})).resolves.toBe(true);
|
await expect(sut.handleQueueDetectFaces({})).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
expect(configMock.load).toHaveBeenCalled();
|
expect(configMock.load).toHaveBeenCalled();
|
||||||
@ -530,19 +531,19 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueRecognizeFaces', () => {
|
describe('handleQueueRecognizeFaces', () => {
|
||||||
it('should return if machine learning is disabled', async () => {
|
it('should skip if machine learning is disabled', async () => {
|
||||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||||
|
|
||||||
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(true);
|
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
expect(configMock.load).toHaveBeenCalled();
|
expect(configMock.load).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return if recognition jobs are already queued', async () => {
|
it('should skip if recognition jobs are already queued', async () => {
|
||||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 1, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 1, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||||
|
|
||||||
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(true);
|
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -612,10 +613,10 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleDetectFaces', () => {
|
describe('handleDetectFaces', () => {
|
||||||
it('should return if machine learning is disabled', async () => {
|
it('should skip if machine learning is disabled', async () => {
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||||
|
|
||||||
await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(true);
|
await expect(sut.handleDetectFaces({ id: 'foo' })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||||
expect(configMock.load).toHaveBeenCalled();
|
expect(configMock.load).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -701,31 +702,31 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleRecognizeFaces', () => {
|
describe('handleRecognizeFaces', () => {
|
||||||
it('should return false if face does not exist', async () => {
|
it('should fail if face does not exist', async () => {
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(null);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(null);
|
||||||
|
|
||||||
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(false);
|
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED);
|
||||||
|
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
expect(personMock.createFaces).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if face does not have asset', async () => {
|
it('should fail if face does not have asset', async () => {
|
||||||
const face = { ...faceStub.face1, asset: null } as AssetFaceEntity & { asset: null };
|
const face = { ...faceStub.face1, asset: null } as AssetFaceEntity & { asset: null };
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(face);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(face);
|
||||||
|
|
||||||
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(false);
|
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.FAILED);
|
||||||
|
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
expect(personMock.createFaces).not.toHaveBeenCalled();
|
expect(personMock.createFaces).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if face already has an assigned person', async () => {
|
it('should skip if face already has an assigned person', async () => {
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||||
|
|
||||||
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(true);
|
expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.SKIPPED);
|
||||||
|
|
||||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||||
expect(personMock.create).not.toHaveBeenCalled();
|
expect(personMock.create).not.toHaveBeenCalled();
|
||||||
@ -852,10 +853,10 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleGeneratePersonThumbnail', () => {
|
describe('handleGeneratePersonThumbnail', () => {
|
||||||
it('should return if machine learning is disabled', async () => {
|
it('should skip if machine learning is disabled', async () => {
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
||||||
|
|
||||||
await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(true);
|
await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||||
expect(configMock.load).toHaveBeenCalled();
|
expect(configMock.load).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
JobItem,
|
JobItem,
|
||||||
|
JobStatus,
|
||||||
UpdateFacesData,
|
UpdateFacesData,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
@ -265,16 +266,16 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePersonCleanup() {
|
async handlePersonCleanup(): Promise<JobStatus> {
|
||||||
const people = await this.repository.getAllWithoutFaces();
|
const people = await this.repository.getAllWithoutFaces();
|
||||||
await this.delete(people);
|
await this.delete(people);
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueDetectFaces({ force }: IBaseJob) {
|
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
@ -294,13 +295,13 @@ export class PersonService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDetectFaces({ id }: IEntityJob) {
|
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relations = {
|
const relations = {
|
||||||
@ -311,7 +312,7 @@ export class PersonService {
|
|||||||
};
|
};
|
||||||
const [asset] = await this.assetRepository.getByIds([id], relations);
|
const [asset] = await this.assetRepository.getByIds([id], relations);
|
||||||
if (!asset || !asset.resizePath || asset.faces?.length > 0) {
|
if (!asset || !asset.resizePath || asset.faces?.length > 0) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const faces = await this.machineLearningRepository.detectFaces(
|
const faces = await this.machineLearningRepository.detectFaces(
|
||||||
@ -346,13 +347,13 @@ export class PersonService {
|
|||||||
facesRecognizedAt: new Date(),
|
facesRecognizedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueRecognizeFaces({ force }: IBaseJob) {
|
async handleQueueRecognizeFaces({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.jobRepository.waitForQueueCompletion(QueueName.THUMBNAIL_GENERATION, QueueName.FACE_DETECTION);
|
await this.jobRepository.waitForQueueCompletion(QueueName.THUMBNAIL_GENERATION, QueueName.FACE_DETECTION);
|
||||||
@ -364,7 +365,7 @@ export class PersonService {
|
|||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
||||||
);
|
);
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
@ -377,13 +378,13 @@ export class PersonService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleRecognizeFaces({ id, deferred }: IDeferrableJob) {
|
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const face = await this.repository.getFaceByIdWithAssets(
|
const face = await this.repository.getFaceByIdWithAssets(
|
||||||
@ -393,12 +394,12 @@ export class PersonService {
|
|||||||
);
|
);
|
||||||
if (!face || !face.asset) {
|
if (!face || !face.asset) {
|
||||||
this.logger.warn(`Face ${id} not found`);
|
this.logger.warn(`Face ${id} not found`);
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (face.personId) {
|
if (face.personId) {
|
||||||
this.logger.debug(`Face ${id} already has a person assigned`);
|
this.logger.debug(`Face ${id} already has a person assigned`);
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = await this.smartInfoRepository.searchFaces({
|
const matches = await this.smartInfoRepository.searchFaces({
|
||||||
@ -411,7 +412,7 @@ export class PersonService {
|
|||||||
// `matches` also includes the face itself
|
// `matches` also includes the face itself
|
||||||
if (machineLearning.facialRecognition.minFaces > 1 && matches.length <= 1) {
|
if (machineLearning.facialRecognition.minFaces > 1 && matches.length <= 1) {
|
||||||
this.logger.debug(`Face ${id} only matched the face itself, skipping`);
|
this.logger.debug(`Face ${id} only matched the face itself, skipping`);
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`Face ${id} has ${matches.length} matches`);
|
this.logger.debug(`Face ${id} has ${matches.length} matches`);
|
||||||
@ -420,7 +421,7 @@ export class PersonService {
|
|||||||
if (!isCore && !deferred) {
|
if (!isCore && !deferred) {
|
||||||
this.logger.debug(`Deferring non-core face ${id} for later processing`);
|
this.logger.debug(`Deferring non-core face ${id} for later processing`);
|
||||||
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
|
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
let personId = matches.find((match) => match.face.personId)?.face.personId;
|
let personId = matches.find((match) => match.face.personId)?.face.personId;
|
||||||
@ -450,34 +451,34 @@ export class PersonService {
|
|||||||
await this.repository.reassignFaces({ faceIds: [id], newPersonId: personId });
|
await this.repository.reassignFaces({ faceIds: [id], newPersonId: personId });
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePersonMigration({ id }: IEntityJob) {
|
async handlePersonMigration({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const person = await this.repository.getById(id);
|
const person = await this.repository.getById(id);
|
||||||
if (!person) {
|
if (!person) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageCore.movePersonFile(person, PersonPathType.FACE);
|
await this.storageCore.movePersonFile(person, PersonPathType.FACE);
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleGeneratePersonThumbnail(data: IEntityJob) {
|
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning, thumbnail } = await this.configCore.getConfig();
|
const { machineLearning, thumbnail } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const person = await this.repository.getById(data.id);
|
const person = await this.repository.getById(data.id);
|
||||||
if (!person?.faceAssetId) {
|
if (!person?.faceAssetId) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId);
|
const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId);
|
||||||
if (face === null) {
|
if (face === null) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -492,7 +493,7 @@ export class PersonService {
|
|||||||
|
|
||||||
const [asset] = await this.assetRepository.getByIds([assetId]);
|
const [asset] = await this.assetRepository.getByIds([assetId]);
|
||||||
if (!asset?.resizePath) {
|
if (!asset?.resizePath) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
this.logger.verbose(`Cropping face for person: ${person.id}`);
|
this.logger.verbose(`Cropping face for person: ${person.id}`);
|
||||||
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
||||||
@ -533,7 +534,7 @@ export class PersonService {
|
|||||||
await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
|
await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
|
||||||
await this.repository.update({ id: person.id, thumbnailPath });
|
await this.repository.update({ id: person.id, thumbnailPath });
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
|
||||||
|
@ -94,7 +94,13 @@ export type JobItem =
|
|||||||
| { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob }
|
| { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob };
|
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob };
|
||||||
|
|
||||||
export type JobHandler<T = any> = (data: T) => boolean | Promise<boolean>;
|
export enum JobStatus {
|
||||||
|
SUCCESS = 'success',
|
||||||
|
FAILED = 'failed',
|
||||||
|
SKIPPED = 'skipped',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JobHandler<T = any> = (data: T) => Promise<JobStatus>;
|
||||||
export type JobItemHandler = (item: JobItem) => Promise<void>;
|
export type JobItemHandler = (item: JobItem) => Promise<void>;
|
||||||
|
|
||||||
export const IJobRepository = 'IJobRepository';
|
export const IJobRepository = 'IJobRepository';
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
IMachineLearningRepository,
|
IMachineLearningRepository,
|
||||||
ISearchRepository,
|
ISearchRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
|
JobStatus,
|
||||||
WithoutProperty,
|
WithoutProperty,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { SystemConfigCore } from '../system-config';
|
import { SystemConfigCore } from '../system-config';
|
||||||
@ -44,10 +45,10 @@ export class SmartInfoService {
|
|||||||
await this.jobRepository.resume(QueueName.SMART_SEARCH);
|
await this.jobRepository.resume(QueueName.SMART_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueEncodeClip({ force }: IBaseJob) {
|
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.clip.enabled) {
|
if (!machineLearning.enabled || !machineLearning.clip.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
@ -66,22 +67,22 @@ export class SmartInfoService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEncodeClip({ id }: IEntityJob) {
|
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
if (!machineLearning.enabled || !machineLearning.clip.enabled) {
|
if (!machineLearning.enabled || !machineLearning.clip.enabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!asset.resizePath) {
|
if (!asset.resizePath) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipEmbedding = await this.machineLearning.encodeImage(
|
const clipEmbedding = await this.machineLearning.encodeImage(
|
||||||
@ -97,6 +98,6 @@ export class SmartInfoService {
|
|||||||
|
|
||||||
await this.repository.upsert({ assetId: asset.id }, clipEmbedding);
|
await this.repository.upsert({ assetId: asset.id }, clipEmbedding);
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
|
JobStatus,
|
||||||
StorageTemplateService,
|
StorageTemplateService,
|
||||||
defaults,
|
defaults,
|
||||||
} from '@app/domain';
|
} from '@app/domain';
|
||||||
@ -76,7 +77,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
describe('handleMigrationSingle', () => {
|
describe('handleMigrationSingle', () => {
|
||||||
it('should skip when storage template is disabled', async () => {
|
it('should skip when storage template is disabled', async () => {
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.STORAGE_TEMPLATE_ENABLED, value: false }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.STORAGE_TEMPLATE_ENABLED, value: false }]);
|
||||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true);
|
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
||||||
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
expect(storageMock.checkFileExists).not.toHaveBeenCalled();
|
||||||
expect(storageMock.rename).not.toHaveBeenCalled();
|
expect(storageMock.rename).not.toHaveBeenCalled();
|
||||||
@ -138,7 +139,9 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath: newMotionPicturePath,
|
newPath: newMotionPicturePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.handleMigrationSingle({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(true);
|
await expect(sut.handleMigrationSingle({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe(
|
||||||
|
JobStatus.SUCCESS,
|
||||||
|
);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true });
|
||||||
@ -190,7 +193,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath,
|
newPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true);
|
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
||||||
@ -247,7 +250,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath,
|
newPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true);
|
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
||||||
@ -298,7 +301,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath,
|
newPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true);
|
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(1);
|
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(1);
|
||||||
@ -364,7 +367,7 @@ describe(StorageTemplateService.name, () => {
|
|||||||
newPath,
|
newPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(true);
|
await expect(sut.handleMigrationSingle({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true });
|
||||||
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
expect(storageMock.checkFileExists).toHaveBeenCalledTimes(3);
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
|
JobStatus,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { StorageCore, StorageFolder } from '../storage';
|
import { StorageCore, StorageFolder } from '../storage';
|
||||||
import {
|
import {
|
||||||
@ -85,16 +86,16 @@ export class StorageTemplateService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleMigrationSingle({ id }: IEntityJob) {
|
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig();
|
||||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||||
if (!storageTemplateEnabled) {
|
if (!storageTemplateEnabled) {
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.get(asset.ownerId, {});
|
const user = await this.userRepository.get(asset.ownerId, {});
|
||||||
@ -106,21 +107,21 @@ export class StorageTemplateService {
|
|||||||
if (asset.livePhotoVideoId) {
|
if (asset.livePhotoVideoId) {
|
||||||
const [livePhotoVideo] = await this.assetRepository.getByIds([asset.livePhotoVideoId], { exifInfo: true });
|
const [livePhotoVideo] = await this.assetRepository.getByIds([asset.livePhotoVideoId], { exifInfo: true });
|
||||||
if (!livePhotoVideo) {
|
if (!livePhotoVideo) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath);
|
const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath);
|
||||||
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename });
|
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename });
|
||||||
}
|
}
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleMigration() {
|
async handleMigration(): Promise<JobStatus> {
|
||||||
this.logger.log('Starting storage template migration');
|
this.logger.log('Starting storage template migration');
|
||||||
const { storageTemplate } = await this.configCore.getConfig();
|
const { storageTemplate } = await this.configCore.getConfig();
|
||||||
const { enabled } = storageTemplate;
|
const { enabled } = storageTemplate;
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
this.logger.log('Storage template migration disabled, skipping');
|
this.logger.log('Storage template migration disabled, skipping');
|
||||||
return true;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
this.assetRepository.getAll(pagination, { withExif: true }),
|
this.assetRepository.getAll(pagination, { withExif: true }),
|
||||||
@ -142,7 +143,7 @@ export class StorageTemplateService {
|
|||||||
|
|
||||||
this.logger.log('Finished storage template migration');
|
this.logger.log('Finished storage template migration');
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ImmichLogger } from '@app/infra/logger';
|
import { ImmichLogger } from '@app/infra/logger';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IDeleteFilesJob } from '../job';
|
import { IDeleteFilesJob } from '../job';
|
||||||
import { IStorageRepository } from '../repositories';
|
import { IStorageRepository, JobStatus } from '../repositories';
|
||||||
import { StorageCore, StorageFolder } from './storage.core';
|
import { StorageCore, StorageFolder } from './storage.core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -31,6 +31,6 @@ export class StorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ISystemConfigRepository,
|
ISystemConfigRepository,
|
||||||
IUserRepository,
|
IUserRepository,
|
||||||
|
JobStatus,
|
||||||
UserFindOptions,
|
UserFindOptions,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { StorageCore, StorageFolder } from '../storage';
|
import { StorageCore, StorageFolder } from '../storage';
|
||||||
@ -143,12 +144,12 @@ export class UserService {
|
|||||||
return { admin, password, provided: !!providedPassword };
|
return { admin, password, provided: !!providedPassword };
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUserSyncUsage() {
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||||
await this.userRepository.syncUsage();
|
await this.userRepository.syncUsage();
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUserDeleteCheck() {
|
async handleUserDeleteCheck(): Promise<JobStatus> {
|
||||||
const users = await this.userRepository.getDeletedUsers();
|
const users = await this.userRepository.getDeletedUsers();
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig();
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
@ -158,20 +159,20 @@ export class UserService {
|
|||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleUserDelete({ id, force }: IEntityJob) {
|
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig();
|
||||||
const user = await this.userRepository.get(id, { withDeleted: true });
|
const user = await this.userRepository.get(id, { withDeleted: true });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// just for extra protection here
|
// just for extra protection here
|
||||||
if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
|
if (!force && !this.isReadyForDeletion(user, config.user.deleteDelay)) {
|
||||||
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
|
this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`);
|
||||||
return false;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Deleting user: ${user.id}`);
|
this.logger.log(`Deleting user: ${user.id}`);
|
||||||
@ -193,7 +194,7 @@ export class UserService {
|
|||||||
await this.albumRepository.deleteAll(user.id);
|
await this.albumRepository.deleteAll(user.id);
|
||||||
await this.userRepository.delete(user, true);
|
await this.userRepository.delete(user, true);
|
||||||
|
|
||||||
return true;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isReadyForDeletion(user: UserEntity, deleteDelay: number): boolean {
|
private isReadyForDeletion(user: UserEntity, deleteDelay: number): boolean {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user