mirror of
https://github.com/immich-app/immich.git
synced 2025-11-18 04:23:19 -05:00
refactor: job vs queue naming (#23902)
This commit is contained in:
parent
1200bfad13
commit
d784d431d0
@ -1,4 +1,4 @@
|
||||
import { JobCommand, JobName, LoginResponseDto, updateConfig } from '@immich/sdk';
|
||||
import { LoginResponseDto, QueueCommand, QueueName, updateConfig } from '@immich/sdk';
|
||||
import { cpSync, rmSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { basename } from 'node:path';
|
||||
@ -17,28 +17,28 @@ describe('/jobs', () => {
|
||||
|
||||
describe('PUT /jobs', () => {
|
||||
afterEach(async () => {
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.FaceDetection, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.FaceDetection, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.SmartSearch, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.SmartSearch, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.DuplicateDetection, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.DuplicateDetection, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
@ -59,8 +59,8 @@ describe('/jobs', () => {
|
||||
it('should queue metadata extraction for missing assets', async () => {
|
||||
const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Pause,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
|
||||
command: QueueCommand.Pause,
|
||||
force: false,
|
||||
});
|
||||
|
||||
@ -77,20 +77,20 @@ describe('/jobs', () => {
|
||||
expect(asset.exifInfo?.make).toBeNull();
|
||||
}
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Empty,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
|
||||
command: QueueCommand.Empty,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Start,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
|
||||
command: QueueCommand.Start,
|
||||
force: false,
|
||||
});
|
||||
|
||||
@ -124,8 +124,8 @@ describe('/jobs', () => {
|
||||
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, path);
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Start,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
|
||||
command: QueueCommand.Start,
|
||||
force: false,
|
||||
});
|
||||
|
||||
@ -144,8 +144,8 @@ describe('/jobs', () => {
|
||||
it('should queue thumbnail extraction for assets missing thumbs', async () => {
|
||||
const path = `${testAssetDir}/albums/nature/tanners_ridge.jpg`;
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Pause,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Pause,
|
||||
force: false,
|
||||
});
|
||||
|
||||
@ -153,32 +153,32 @@ describe('/jobs', () => {
|
||||
assetData: { bytes: await readFile(path), filename: basename(path) },
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
|
||||
|
||||
const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
|
||||
expect(assetBefore.thumbhash).toBeNull();
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Empty,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Empty,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Start,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Start,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
|
||||
|
||||
const assetAfter = await utils.getAssetInfo(admin.accessToken, id);
|
||||
expect(assetAfter.thumbhash).not.toBeNull();
|
||||
@ -193,26 +193,26 @@ describe('/jobs', () => {
|
||||
assetData: { bytes: await readFile(path), filename: basename(path) },
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
|
||||
|
||||
const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/notocactus_minimus.jpg`, path);
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Resume,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
// This runs the missing thumbnail job
|
||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||
command: JobCommand.Start,
|
||||
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
|
||||
command: QueueCommand.Start,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
|
||||
|
||||
const assetAfter = await utils.getAssetInfo(admin.accessToken, id);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
JobName,
|
||||
LoginResponseDto,
|
||||
QueueName,
|
||||
createStack,
|
||||
deleteUserAdmin,
|
||||
getMyUser,
|
||||
@ -328,7 +328,7 @@ describe('/admin/users', () => {
|
||||
{ headers: asBearerAuth(user.accessToken) },
|
||||
);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/admin/users/${user.userId}`)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import {
|
||||
AllJobStatusResponseDto,
|
||||
AssetMediaCreateDto,
|
||||
AssetMediaResponseDto,
|
||||
AssetResponseDto,
|
||||
@ -7,11 +6,12 @@ import {
|
||||
CheckExistingAssetsDto,
|
||||
CreateAlbumDto,
|
||||
CreateLibraryDto,
|
||||
JobCommandDto,
|
||||
JobName,
|
||||
MetadataSearchDto,
|
||||
Permission,
|
||||
PersonCreateDto,
|
||||
QueueCommandDto,
|
||||
QueueName,
|
||||
QueuesResponseDto,
|
||||
SharedLinkCreateDto,
|
||||
UpdateLibraryDto,
|
||||
UserAdminCreateDto,
|
||||
@ -27,14 +27,14 @@ import {
|
||||
createStack,
|
||||
createUserAdmin,
|
||||
deleteAssets,
|
||||
getAllJobsStatus,
|
||||
getAssetInfo,
|
||||
getConfig,
|
||||
getConfigDefaults,
|
||||
getQueuesLegacy,
|
||||
login,
|
||||
runQueueCommandLegacy,
|
||||
scanLibrary,
|
||||
searchAssets,
|
||||
sendJobCommand,
|
||||
setBaseUrl,
|
||||
signUpAdmin,
|
||||
tagAssets,
|
||||
@ -477,8 +477,8 @@ export const utils = {
|
||||
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
||||
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
|
||||
sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
||||
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
|
||||
await context.addCookies([
|
||||
@ -524,13 +524,13 @@ export const utils = {
|
||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
|
||||
isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => {
|
||||
const queues = await getAllJobsStatus({ headers: asBearerAuth(accessToken) });
|
||||
isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseDto) => {
|
||||
const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) });
|
||||
const jobCounts = queues[queue].jobCounts;
|
||||
return !jobCounts.active && !jobCounts.waiting;
|
||||
},
|
||||
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => {
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseDto, ms?: number) => {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
|
||||
|
||||
16
mobile/openapi/README.md
generated
16
mobile/openapi/README.md
generated
@ -149,8 +149,8 @@ Class | Method | HTTP request | Description
|
||||
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces | Retrieve faces for asset
|
||||
*FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} | Re-assign a face to another person
|
||||
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs | Create a manual job
|
||||
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs | Retrieve queue counts and status
|
||||
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} | Run jobs
|
||||
*JobsApi* | [**getQueuesLegacy**](doc//JobsApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
|
||||
*JobsApi* | [**runQueueCommandLegacy**](doc//JobsApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
|
||||
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries | Create a library
|
||||
*LibrariesApi* | [**deleteLibrary**](doc//LibrariesApi.md#deletelibrary) | **DELETE** /libraries/{id} | Delete a library
|
||||
*LibrariesApi* | [**getAllLibraries**](doc//LibrariesApi.md#getalllibraries) | **GET** /libraries | Retrieve libraries
|
||||
@ -318,7 +318,6 @@ Class | Method | HTTP request | Description
|
||||
- [AlbumsAddAssetsResponseDto](doc//AlbumsAddAssetsResponseDto.md)
|
||||
- [AlbumsResponse](doc//AlbumsResponse.md)
|
||||
- [AlbumsUpdate](doc//AlbumsUpdate.md)
|
||||
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
|
||||
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
|
||||
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
|
||||
- [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md)
|
||||
@ -387,13 +386,8 @@ Class | Method | HTTP request | Description
|
||||
- [FoldersResponse](doc//FoldersResponse.md)
|
||||
- [FoldersUpdate](doc//FoldersUpdate.md)
|
||||
- [ImageFormat](doc//ImageFormat.md)
|
||||
- [JobCommand](doc//JobCommand.md)
|
||||
- [JobCommandDto](doc//JobCommandDto.md)
|
||||
- [JobCountsDto](doc//JobCountsDto.md)
|
||||
- [JobCreateDto](doc//JobCreateDto.md)
|
||||
- [JobName](doc//JobName.md)
|
||||
- [JobSettingsDto](doc//JobSettingsDto.md)
|
||||
- [JobStatusDto](doc//JobStatusDto.md)
|
||||
- [LibraryResponseDto](doc//LibraryResponseDto.md)
|
||||
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
|
||||
- [LicenseKeyDto](doc//LicenseKeyDto.md)
|
||||
@ -452,7 +446,13 @@ Class | Method | HTTP request | Description
|
||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||
- [QueueCommand](doc//QueueCommand.md)
|
||||
- [QueueCommandDto](doc//QueueCommandDto.md)
|
||||
- [QueueName](doc//QueueName.md)
|
||||
- [QueueResponseDto](doc//QueueResponseDto.md)
|
||||
- [QueueStatisticsDto](doc//QueueStatisticsDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [QueuesResponseDto](doc//QueuesResponseDto.md)
|
||||
- [RandomSearchDto](doc//RandomSearchDto.md)
|
||||
- [RatingsResponse](doc//RatingsResponse.md)
|
||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||
|
||||
12
mobile/openapi/lib/api.dart
generated
12
mobile/openapi/lib/api.dart
generated
@ -82,7 +82,6 @@ part 'model/albums_add_assets_dto.dart';
|
||||
part 'model/albums_add_assets_response_dto.dart';
|
||||
part 'model/albums_response.dart';
|
||||
part 'model/albums_update.dart';
|
||||
part 'model/all_job_status_response_dto.dart';
|
||||
part 'model/asset_bulk_delete_dto.dart';
|
||||
part 'model/asset_bulk_update_dto.dart';
|
||||
part 'model/asset_bulk_upload_check_dto.dart';
|
||||
@ -151,13 +150,8 @@ part 'model/facial_recognition_config.dart';
|
||||
part 'model/folders_response.dart';
|
||||
part 'model/folders_update.dart';
|
||||
part 'model/image_format.dart';
|
||||
part 'model/job_command.dart';
|
||||
part 'model/job_command_dto.dart';
|
||||
part 'model/job_counts_dto.dart';
|
||||
part 'model/job_create_dto.dart';
|
||||
part 'model/job_name.dart';
|
||||
part 'model/job_settings_dto.dart';
|
||||
part 'model/job_status_dto.dart';
|
||||
part 'model/library_response_dto.dart';
|
||||
part 'model/library_stats_response_dto.dart';
|
||||
part 'model/license_key_dto.dart';
|
||||
@ -216,7 +210,13 @@ part 'model/pin_code_setup_dto.dart';
|
||||
part 'model/places_response_dto.dart';
|
||||
part 'model/purchase_response.dart';
|
||||
part 'model/purchase_update.dart';
|
||||
part 'model/queue_command.dart';
|
||||
part 'model/queue_command_dto.dart';
|
||||
part 'model/queue_name.dart';
|
||||
part 'model/queue_response_dto.dart';
|
||||
part 'model/queue_statistics_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/queues_response_dto.dart';
|
||||
part 'model/random_search_dto.dart';
|
||||
part 'model/ratings_response.dart';
|
||||
part 'model/ratings_update.dart';
|
||||
|
||||
30
mobile/openapi/lib/api/jobs_api.dart
generated
30
mobile/openapi/lib/api/jobs_api.dart
generated
@ -69,7 +69,7 @@ class JobsApi {
|
||||
/// Retrieve the counts of the current queue, as well as the current status.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
Future<Response> getAllJobsStatusWithHttpInfo() async {
|
||||
Future<Response> getQueuesLegacyWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/jobs';
|
||||
|
||||
@ -97,8 +97,8 @@ class JobsApi {
|
||||
/// Retrieve queue counts and status
|
||||
///
|
||||
/// Retrieve the counts of the current queue, as well as the current status.
|
||||
Future<AllJobStatusResponseDto?> getAllJobsStatus() async {
|
||||
final response = await getAllJobsStatusWithHttpInfo();
|
||||
Future<QueuesResponseDto?> getQueuesLegacy() async {
|
||||
final response = await getQueuesLegacyWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@ -106,7 +106,7 @@ class JobsApi {
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AllJobStatusResponseDto',) as AllJobStatusResponseDto;
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseDto',) as QueuesResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
@ -120,16 +120,16 @@ class JobsApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [JobName] id (required):
|
||||
/// * [QueueName] name (required):
|
||||
///
|
||||
/// * [JobCommandDto] jobCommandDto (required):
|
||||
Future<Response> sendJobCommandWithHttpInfo(JobName id, JobCommandDto jobCommandDto,) async {
|
||||
/// * [QueueCommandDto] queueCommandDto (required):
|
||||
Future<Response> runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/jobs/{id}'
|
||||
.replaceAll('{id}', id.toString());
|
||||
final apiPath = r'/jobs/{name}'
|
||||
.replaceAll('{name}', name.toString());
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = jobCommandDto;
|
||||
Object? postBody = queueCommandDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
@ -155,11 +155,11 @@ class JobsApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [JobName] id (required):
|
||||
/// * [QueueName] name (required):
|
||||
///
|
||||
/// * [JobCommandDto] jobCommandDto (required):
|
||||
Future<JobStatusDto?> sendJobCommand(JobName id, JobCommandDto jobCommandDto,) async {
|
||||
final response = await sendJobCommandWithHttpInfo(id, jobCommandDto,);
|
||||
/// * [QueueCommandDto] queueCommandDto (required):
|
||||
Future<QueueResponseDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async {
|
||||
final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@ -167,7 +167,7 @@ class JobsApi {
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'JobStatusDto',) as JobStatusDto;
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
|
||||
24
mobile/openapi/lib/api_client.dart
generated
24
mobile/openapi/lib/api_client.dart
generated
@ -220,8 +220,6 @@ class ApiClient {
|
||||
return AlbumsResponse.fromJson(value);
|
||||
case 'AlbumsUpdate':
|
||||
return AlbumsUpdate.fromJson(value);
|
||||
case 'AllJobStatusResponseDto':
|
||||
return AllJobStatusResponseDto.fromJson(value);
|
||||
case 'AssetBulkDeleteDto':
|
||||
return AssetBulkDeleteDto.fromJson(value);
|
||||
case 'AssetBulkUpdateDto':
|
||||
@ -358,20 +356,10 @@ class ApiClient {
|
||||
return FoldersUpdate.fromJson(value);
|
||||
case 'ImageFormat':
|
||||
return ImageFormatTypeTransformer().decode(value);
|
||||
case 'JobCommand':
|
||||
return JobCommandTypeTransformer().decode(value);
|
||||
case 'JobCommandDto':
|
||||
return JobCommandDto.fromJson(value);
|
||||
case 'JobCountsDto':
|
||||
return JobCountsDto.fromJson(value);
|
||||
case 'JobCreateDto':
|
||||
return JobCreateDto.fromJson(value);
|
||||
case 'JobName':
|
||||
return JobNameTypeTransformer().decode(value);
|
||||
case 'JobSettingsDto':
|
||||
return JobSettingsDto.fromJson(value);
|
||||
case 'JobStatusDto':
|
||||
return JobStatusDto.fromJson(value);
|
||||
case 'LibraryResponseDto':
|
||||
return LibraryResponseDto.fromJson(value);
|
||||
case 'LibraryStatsResponseDto':
|
||||
@ -488,8 +476,20 @@ class ApiClient {
|
||||
return PurchaseResponse.fromJson(value);
|
||||
case 'PurchaseUpdate':
|
||||
return PurchaseUpdate.fromJson(value);
|
||||
case 'QueueCommand':
|
||||
return QueueCommandTypeTransformer().decode(value);
|
||||
case 'QueueCommandDto':
|
||||
return QueueCommandDto.fromJson(value);
|
||||
case 'QueueName':
|
||||
return QueueNameTypeTransformer().decode(value);
|
||||
case 'QueueResponseDto':
|
||||
return QueueResponseDto.fromJson(value);
|
||||
case 'QueueStatisticsDto':
|
||||
return QueueStatisticsDto.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'QueuesResponseDto':
|
||||
return QueuesResponseDto.fromJson(value);
|
||||
case 'RandomSearchDto':
|
||||
return RandomSearchDto.fromJson(value);
|
||||
case 'RatingsResponse':
|
||||
|
||||
12
mobile/openapi/lib/api_helper.dart
generated
12
mobile/openapi/lib/api_helper.dart
generated
@ -94,12 +94,6 @@ String parameterToString(dynamic value) {
|
||||
if (value is ImageFormat) {
|
||||
return ImageFormatTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is JobCommand) {
|
||||
return JobCommandTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is JobName) {
|
||||
return JobNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is LogLevel) {
|
||||
return LogLevelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
@ -127,6 +121,12 @@ String parameterToString(dynamic value) {
|
||||
if (value is Permission) {
|
||||
return PermissionTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is QueueCommand) {
|
||||
return QueueCommandTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is QueueName) {
|
||||
return QueueNameTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReactionLevel) {
|
||||
return ReactionLevelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
94
mobile/openapi/lib/model/job_command.dart
generated
94
mobile/openapi/lib/model/job_command.dart
generated
@ -1,94 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class JobCommand {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const JobCommand._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const start = JobCommand._(r'start');
|
||||
static const pause = JobCommand._(r'pause');
|
||||
static const resume = JobCommand._(r'resume');
|
||||
static const empty = JobCommand._(r'empty');
|
||||
static const clearFailed = JobCommand._(r'clear-failed');
|
||||
|
||||
/// List of all possible values in this [enum][JobCommand].
|
||||
static const values = <JobCommand>[
|
||||
start,
|
||||
pause,
|
||||
resume,
|
||||
empty,
|
||||
clearFailed,
|
||||
];
|
||||
|
||||
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
|
||||
|
||||
static List<JobCommand> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <JobCommand>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = JobCommand.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [JobCommand] to String,
|
||||
/// and [decode] dynamic data back to [JobCommand].
|
||||
class JobCommandTypeTransformer {
|
||||
factory JobCommandTypeTransformer() => _instance ??= const JobCommandTypeTransformer._();
|
||||
|
||||
const JobCommandTypeTransformer._();
|
||||
|
||||
String encode(JobCommand data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a JobCommand.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
JobCommand? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'start': return JobCommand.start;
|
||||
case r'pause': return JobCommand.pause;
|
||||
case r'resume': return JobCommand.resume;
|
||||
case r'empty': return JobCommand.empty;
|
||||
case r'clear-failed': return JobCommand.clearFailed;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [JobCommandTypeTransformer] instance.
|
||||
static JobCommandTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
127
mobile/openapi/lib/model/job_name.dart
generated
127
mobile/openapi/lib/model/job_name.dart
generated
@ -1,127 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class JobName {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const JobName._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const thumbnailGeneration = JobName._(r'thumbnailGeneration');
|
||||
static const metadataExtraction = JobName._(r'metadataExtraction');
|
||||
static const videoConversion = JobName._(r'videoConversion');
|
||||
static const faceDetection = JobName._(r'faceDetection');
|
||||
static const facialRecognition = JobName._(r'facialRecognition');
|
||||
static const smartSearch = JobName._(r'smartSearch');
|
||||
static const duplicateDetection = JobName._(r'duplicateDetection');
|
||||
static const backgroundTask = JobName._(r'backgroundTask');
|
||||
static const storageTemplateMigration = JobName._(r'storageTemplateMigration');
|
||||
static const migration = JobName._(r'migration');
|
||||
static const search = JobName._(r'search');
|
||||
static const sidecar = JobName._(r'sidecar');
|
||||
static const library_ = JobName._(r'library');
|
||||
static const notifications = JobName._(r'notifications');
|
||||
static const backupDatabase = JobName._(r'backupDatabase');
|
||||
static const ocr = JobName._(r'ocr');
|
||||
|
||||
/// List of all possible values in this [enum][JobName].
|
||||
static const values = <JobName>[
|
||||
thumbnailGeneration,
|
||||
metadataExtraction,
|
||||
videoConversion,
|
||||
faceDetection,
|
||||
facialRecognition,
|
||||
smartSearch,
|
||||
duplicateDetection,
|
||||
backgroundTask,
|
||||
storageTemplateMigration,
|
||||
migration,
|
||||
search,
|
||||
sidecar,
|
||||
library_,
|
||||
notifications,
|
||||
backupDatabase,
|
||||
ocr,
|
||||
];
|
||||
|
||||
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
|
||||
|
||||
static List<JobName> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <JobName>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = JobName.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [JobName] to String,
|
||||
/// and [decode] dynamic data back to [JobName].
|
||||
class JobNameTypeTransformer {
|
||||
factory JobNameTypeTransformer() => _instance ??= const JobNameTypeTransformer._();
|
||||
|
||||
const JobNameTypeTransformer._();
|
||||
|
||||
String encode(JobName data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a JobName.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
JobName? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'thumbnailGeneration': return JobName.thumbnailGeneration;
|
||||
case r'metadataExtraction': return JobName.metadataExtraction;
|
||||
case r'videoConversion': return JobName.videoConversion;
|
||||
case r'faceDetection': return JobName.faceDetection;
|
||||
case r'facialRecognition': return JobName.facialRecognition;
|
||||
case r'smartSearch': return JobName.smartSearch;
|
||||
case r'duplicateDetection': return JobName.duplicateDetection;
|
||||
case r'backgroundTask': return JobName.backgroundTask;
|
||||
case r'storageTemplateMigration': return JobName.storageTemplateMigration;
|
||||
case r'migration': return JobName.migration;
|
||||
case r'search': return JobName.search;
|
||||
case r'sidecar': return JobName.sidecar;
|
||||
case r'library': return JobName.library_;
|
||||
case r'notifications': return JobName.notifications;
|
||||
case r'backupDatabase': return JobName.backupDatabase;
|
||||
case r'ocr': return JobName.ocr;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [JobNameTypeTransformer] instance.
|
||||
static JobNameTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
94
mobile/openapi/lib/model/queue_command.dart
generated
Normal file
94
mobile/openapi/lib/model/queue_command.dart
generated
Normal file
@ -0,0 +1,94 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class QueueCommand {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const QueueCommand._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const start = QueueCommand._(r'start');
|
||||
static const pause = QueueCommand._(r'pause');
|
||||
static const resume = QueueCommand._(r'resume');
|
||||
static const empty = QueueCommand._(r'empty');
|
||||
static const clearFailed = QueueCommand._(r'clear-failed');
|
||||
|
||||
/// List of all possible values in this [enum][QueueCommand].
|
||||
static const values = <QueueCommand>[
|
||||
start,
|
||||
pause,
|
||||
resume,
|
||||
empty,
|
||||
clearFailed,
|
||||
];
|
||||
|
||||
static QueueCommand? fromJson(dynamic value) => QueueCommandTypeTransformer().decode(value);
|
||||
|
||||
static List<QueueCommand> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueCommand>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = QueueCommand.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [QueueCommand] to String,
|
||||
/// and [decode] dynamic data back to [QueueCommand].
|
||||
class QueueCommandTypeTransformer {
|
||||
factory QueueCommandTypeTransformer() => _instance ??= const QueueCommandTypeTransformer._();
|
||||
|
||||
const QueueCommandTypeTransformer._();
|
||||
|
||||
String encode(QueueCommand data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a QueueCommand.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
QueueCommand? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'start': return QueueCommand.start;
|
||||
case r'pause': return QueueCommand.pause;
|
||||
case r'resume': return QueueCommand.resume;
|
||||
case r'empty': return QueueCommand.empty;
|
||||
case r'clear-failed': return QueueCommand.clearFailed;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [QueueCommandTypeTransformer] instance.
|
||||
static QueueCommandTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
@ -10,14 +10,14 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class JobCommandDto {
|
||||
/// Returns a new [JobCommandDto] instance.
|
||||
JobCommandDto({
|
||||
class QueueCommandDto {
|
||||
/// Returns a new [QueueCommandDto] instance.
|
||||
QueueCommandDto({
|
||||
required this.command,
|
||||
this.force,
|
||||
});
|
||||
|
||||
JobCommand command;
|
||||
QueueCommand command;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@ -28,7 +28,7 @@ class JobCommandDto {
|
||||
bool? force;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is JobCommandDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueueCommandDto &&
|
||||
other.command == command &&
|
||||
other.force == force;
|
||||
|
||||
@ -39,7 +39,7 @@ class JobCommandDto {
|
||||
(force == null ? 0 : force!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'JobCommandDto[command=$command, force=$force]';
|
||||
String toString() => 'QueueCommandDto[command=$command, force=$force]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -52,27 +52,27 @@ class JobCommandDto {
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [JobCommandDto] instance and imports its values from
|
||||
/// Returns a new [QueueCommandDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static JobCommandDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "JobCommandDto");
|
||||
static QueueCommandDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueueCommandDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return JobCommandDto(
|
||||
command: JobCommand.fromJson(json[r'command'])!,
|
||||
return QueueCommandDto(
|
||||
command: QueueCommand.fromJson(json[r'command'])!,
|
||||
force: mapValueOfType<bool>(json, r'force'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<JobCommandDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <JobCommandDto>[];
|
||||
static List<QueueCommandDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueCommandDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = JobCommandDto.fromJson(row);
|
||||
final value = QueueCommandDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@ -81,12 +81,12 @@ class JobCommandDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, JobCommandDto> mapFromJson(dynamic json) {
|
||||
final map = <String, JobCommandDto>{};
|
||||
static Map<String, QueueCommandDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueueCommandDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = JobCommandDto.fromJson(entry.value);
|
||||
final value = QueueCommandDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@ -95,14 +95,14 @@ class JobCommandDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of JobCommandDto-objects as value to a dart map
|
||||
static Map<String, List<JobCommandDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<JobCommandDto>>{};
|
||||
// maps a json object with a list of QueueCommandDto-objects as value to a dart map
|
||||
static Map<String, List<QueueCommandDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueueCommandDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = JobCommandDto.listFromJson(entry.value, growable: growable,);
|
||||
map[entry.key] = QueueCommandDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
127
mobile/openapi/lib/model/queue_name.dart
generated
Normal file
127
mobile/openapi/lib/model/queue_name.dart
generated
Normal file
@ -0,0 +1,127 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class QueueName {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const QueueName._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const thumbnailGeneration = QueueName._(r'thumbnailGeneration');
|
||||
static const metadataExtraction = QueueName._(r'metadataExtraction');
|
||||
static const videoConversion = QueueName._(r'videoConversion');
|
||||
static const faceDetection = QueueName._(r'faceDetection');
|
||||
static const facialRecognition = QueueName._(r'facialRecognition');
|
||||
static const smartSearch = QueueName._(r'smartSearch');
|
||||
static const duplicateDetection = QueueName._(r'duplicateDetection');
|
||||
static const backgroundTask = QueueName._(r'backgroundTask');
|
||||
static const storageTemplateMigration = QueueName._(r'storageTemplateMigration');
|
||||
static const migration = QueueName._(r'migration');
|
||||
static const search = QueueName._(r'search');
|
||||
static const sidecar = QueueName._(r'sidecar');
|
||||
static const library_ = QueueName._(r'library');
|
||||
static const notifications = QueueName._(r'notifications');
|
||||
static const backupDatabase = QueueName._(r'backupDatabase');
|
||||
static const ocr = QueueName._(r'ocr');
|
||||
|
||||
/// List of all possible values in this [enum][QueueName].
|
||||
static const values = <QueueName>[
|
||||
thumbnailGeneration,
|
||||
metadataExtraction,
|
||||
videoConversion,
|
||||
faceDetection,
|
||||
facialRecognition,
|
||||
smartSearch,
|
||||
duplicateDetection,
|
||||
backgroundTask,
|
||||
storageTemplateMigration,
|
||||
migration,
|
||||
search,
|
||||
sidecar,
|
||||
library_,
|
||||
notifications,
|
||||
backupDatabase,
|
||||
ocr,
|
||||
];
|
||||
|
||||
static QueueName? fromJson(dynamic value) => QueueNameTypeTransformer().decode(value);
|
||||
|
||||
static List<QueueName> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueName>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = QueueName.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [QueueName] to String,
|
||||
/// and [decode] dynamic data back to [QueueName].
|
||||
class QueueNameTypeTransformer {
|
||||
factory QueueNameTypeTransformer() => _instance ??= const QueueNameTypeTransformer._();
|
||||
|
||||
const QueueNameTypeTransformer._();
|
||||
|
||||
String encode(QueueName data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a QueueName.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
QueueName? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'thumbnailGeneration': return QueueName.thumbnailGeneration;
|
||||
case r'metadataExtraction': return QueueName.metadataExtraction;
|
||||
case r'videoConversion': return QueueName.videoConversion;
|
||||
case r'faceDetection': return QueueName.faceDetection;
|
||||
case r'facialRecognition': return QueueName.facialRecognition;
|
||||
case r'smartSearch': return QueueName.smartSearch;
|
||||
case r'duplicateDetection': return QueueName.duplicateDetection;
|
||||
case r'backgroundTask': return QueueName.backgroundTask;
|
||||
case r'storageTemplateMigration': return QueueName.storageTemplateMigration;
|
||||
case r'migration': return QueueName.migration;
|
||||
case r'search': return QueueName.search;
|
||||
case r'sidecar': return QueueName.sidecar;
|
||||
case r'library': return QueueName.library_;
|
||||
case r'notifications': return QueueName.notifications;
|
||||
case r'backupDatabase': return QueueName.backupDatabase;
|
||||
case r'ocr': return QueueName.ocr;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [QueueNameTypeTransformer] instance.
|
||||
static QueueNameTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
@ -10,19 +10,19 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class JobStatusDto {
|
||||
/// Returns a new [JobStatusDto] instance.
|
||||
JobStatusDto({
|
||||
class QueueResponseDto {
|
||||
/// Returns a new [QueueResponseDto] instance.
|
||||
QueueResponseDto({
|
||||
required this.jobCounts,
|
||||
required this.queueStatus,
|
||||
});
|
||||
|
||||
JobCountsDto jobCounts;
|
||||
QueueStatisticsDto jobCounts;
|
||||
|
||||
QueueStatusDto queueStatus;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is JobStatusDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto &&
|
||||
other.jobCounts == jobCounts &&
|
||||
other.queueStatus == queueStatus;
|
||||
|
||||
@ -33,7 +33,7 @@ class JobStatusDto {
|
||||
(queueStatus.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'JobStatusDto[jobCounts=$jobCounts, queueStatus=$queueStatus]';
|
||||
String toString() => 'QueueResponseDto[jobCounts=$jobCounts, queueStatus=$queueStatus]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -42,27 +42,27 @@ class JobStatusDto {
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [JobStatusDto] instance and imports its values from
|
||||
/// Returns a new [QueueResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static JobStatusDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "JobStatusDto");
|
||||
static QueueResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueueResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return JobStatusDto(
|
||||
jobCounts: JobCountsDto.fromJson(json[r'jobCounts'])!,
|
||||
return QueueResponseDto(
|
||||
jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!,
|
||||
queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<JobStatusDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <JobStatusDto>[];
|
||||
static List<QueueResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = JobStatusDto.fromJson(row);
|
||||
final value = QueueResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@ -71,12 +71,12 @@ class JobStatusDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, JobStatusDto> mapFromJson(dynamic json) {
|
||||
final map = <String, JobStatusDto>{};
|
||||
static Map<String, QueueResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueueResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = JobStatusDto.fromJson(entry.value);
|
||||
final value = QueueResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@ -85,14 +85,14 @@ class JobStatusDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of JobStatusDto-objects as value to a dart map
|
||||
static Map<String, List<JobStatusDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<JobStatusDto>>{};
|
||||
// maps a json object with a list of QueueResponseDto-objects as value to a dart map
|
||||
static Map<String, List<QueueResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueueResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = JobStatusDto.listFromJson(entry.value, growable: growable,);
|
||||
map[entry.key] = QueueResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@ -10,9 +10,9 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class JobCountsDto {
|
||||
/// Returns a new [JobCountsDto] instance.
|
||||
JobCountsDto({
|
||||
class QueueStatisticsDto {
|
||||
/// Returns a new [QueueStatisticsDto] instance.
|
||||
QueueStatisticsDto({
|
||||
required this.active,
|
||||
required this.completed,
|
||||
required this.delayed,
|
||||
@ -34,7 +34,7 @@ class JobCountsDto {
|
||||
int waiting;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is JobCountsDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueueStatisticsDto &&
|
||||
other.active == active &&
|
||||
other.completed == completed &&
|
||||
other.delayed == delayed &&
|
||||
@ -53,7 +53,7 @@ class JobCountsDto {
|
||||
(waiting.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'JobCountsDto[active=$active, completed=$completed, delayed=$delayed, failed=$failed, paused=$paused, waiting=$waiting]';
|
||||
String toString() => 'QueueStatisticsDto[active=$active, completed=$completed, delayed=$delayed, failed=$failed, paused=$paused, waiting=$waiting]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -66,15 +66,15 @@ class JobCountsDto {
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [JobCountsDto] instance and imports its values from
|
||||
/// Returns a new [QueueStatisticsDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static JobCountsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "JobCountsDto");
|
||||
static QueueStatisticsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueueStatisticsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return JobCountsDto(
|
||||
return QueueStatisticsDto(
|
||||
active: mapValueOfType<int>(json, r'active')!,
|
||||
completed: mapValueOfType<int>(json, r'completed')!,
|
||||
delayed: mapValueOfType<int>(json, r'delayed')!,
|
||||
@ -86,11 +86,11 @@ class JobCountsDto {
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<JobCountsDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <JobCountsDto>[];
|
||||
static List<QueueStatisticsDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueueStatisticsDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = JobCountsDto.fromJson(row);
|
||||
final value = QueueStatisticsDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@ -99,12 +99,12 @@ class JobCountsDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, JobCountsDto> mapFromJson(dynamic json) {
|
||||
final map = <String, JobCountsDto>{};
|
||||
static Map<String, QueueStatisticsDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueueStatisticsDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = JobCountsDto.fromJson(entry.value);
|
||||
final value = QueueStatisticsDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@ -113,14 +113,14 @@ class JobCountsDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of JobCountsDto-objects as value to a dart map
|
||||
static Map<String, List<JobCountsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<JobCountsDto>>{};
|
||||
// maps a json object with a list of QueueStatisticsDto-objects as value to a dart map
|
||||
static Map<String, List<QueueStatisticsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueueStatisticsDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = JobCountsDto.listFromJson(entry.value, growable: growable,);
|
||||
map[entry.key] = QueueStatisticsDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@ -10,9 +10,9 @@
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AllJobStatusResponseDto {
|
||||
/// Returns a new [AllJobStatusResponseDto] instance.
|
||||
AllJobStatusResponseDto({
|
||||
class QueuesResponseDto {
|
||||
/// Returns a new [QueuesResponseDto] instance.
|
||||
QueuesResponseDto({
|
||||
required this.backgroundTask,
|
||||
required this.backupDatabase,
|
||||
required this.duplicateDetection,
|
||||
@ -31,40 +31,40 @@ class AllJobStatusResponseDto {
|
||||
required this.videoConversion,
|
||||
});
|
||||
|
||||
JobStatusDto backgroundTask;
|
||||
QueueResponseDto backgroundTask;
|
||||
|
||||
JobStatusDto backupDatabase;
|
||||
QueueResponseDto backupDatabase;
|
||||
|
||||
JobStatusDto duplicateDetection;
|
||||
QueueResponseDto duplicateDetection;
|
||||
|
||||
JobStatusDto faceDetection;
|
||||
QueueResponseDto faceDetection;
|
||||
|
||||
JobStatusDto facialRecognition;
|
||||
QueueResponseDto facialRecognition;
|
||||
|
||||
JobStatusDto library_;
|
||||
QueueResponseDto library_;
|
||||
|
||||
JobStatusDto metadataExtraction;
|
||||
QueueResponseDto metadataExtraction;
|
||||
|
||||
JobStatusDto migration;
|
||||
QueueResponseDto migration;
|
||||
|
||||
JobStatusDto notifications;
|
||||
QueueResponseDto notifications;
|
||||
|
||||
JobStatusDto ocr;
|
||||
QueueResponseDto ocr;
|
||||
|
||||
JobStatusDto search;
|
||||
QueueResponseDto search;
|
||||
|
||||
JobStatusDto sidecar;
|
||||
QueueResponseDto sidecar;
|
||||
|
||||
JobStatusDto smartSearch;
|
||||
QueueResponseDto smartSearch;
|
||||
|
||||
JobStatusDto storageTemplateMigration;
|
||||
QueueResponseDto storageTemplateMigration;
|
||||
|
||||
JobStatusDto thumbnailGeneration;
|
||||
QueueResponseDto thumbnailGeneration;
|
||||
|
||||
JobStatusDto videoConversion;
|
||||
QueueResponseDto videoConversion;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AllJobStatusResponseDto &&
|
||||
bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto &&
|
||||
other.backgroundTask == backgroundTask &&
|
||||
other.backupDatabase == backupDatabase &&
|
||||
other.duplicateDetection == duplicateDetection &&
|
||||
@ -103,7 +103,7 @@ class AllJobStatusResponseDto {
|
||||
(videoConversion.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
String toString() => 'QueuesResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -126,41 +126,41 @@ class AllJobStatusResponseDto {
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AllJobStatusResponseDto] instance and imports its values from
|
||||
/// Returns a new [QueuesResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AllJobStatusResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "AllJobStatusResponseDto");
|
||||
static QueuesResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "QueuesResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AllJobStatusResponseDto(
|
||||
backgroundTask: JobStatusDto.fromJson(json[r'backgroundTask'])!,
|
||||
backupDatabase: JobStatusDto.fromJson(json[r'backupDatabase'])!,
|
||||
duplicateDetection: JobStatusDto.fromJson(json[r'duplicateDetection'])!,
|
||||
faceDetection: JobStatusDto.fromJson(json[r'faceDetection'])!,
|
||||
facialRecognition: JobStatusDto.fromJson(json[r'facialRecognition'])!,
|
||||
library_: JobStatusDto.fromJson(json[r'library'])!,
|
||||
metadataExtraction: JobStatusDto.fromJson(json[r'metadataExtraction'])!,
|
||||
migration: JobStatusDto.fromJson(json[r'migration'])!,
|
||||
notifications: JobStatusDto.fromJson(json[r'notifications'])!,
|
||||
ocr: JobStatusDto.fromJson(json[r'ocr'])!,
|
||||
search: JobStatusDto.fromJson(json[r'search'])!,
|
||||
sidecar: JobStatusDto.fromJson(json[r'sidecar'])!,
|
||||
smartSearch: JobStatusDto.fromJson(json[r'smartSearch'])!,
|
||||
storageTemplateMigration: JobStatusDto.fromJson(json[r'storageTemplateMigration'])!,
|
||||
thumbnailGeneration: JobStatusDto.fromJson(json[r'thumbnailGeneration'])!,
|
||||
videoConversion: JobStatusDto.fromJson(json[r'videoConversion'])!,
|
||||
return QueuesResponseDto(
|
||||
backgroundTask: QueueResponseDto.fromJson(json[r'backgroundTask'])!,
|
||||
backupDatabase: QueueResponseDto.fromJson(json[r'backupDatabase'])!,
|
||||
duplicateDetection: QueueResponseDto.fromJson(json[r'duplicateDetection'])!,
|
||||
faceDetection: QueueResponseDto.fromJson(json[r'faceDetection'])!,
|
||||
facialRecognition: QueueResponseDto.fromJson(json[r'facialRecognition'])!,
|
||||
library_: QueueResponseDto.fromJson(json[r'library'])!,
|
||||
metadataExtraction: QueueResponseDto.fromJson(json[r'metadataExtraction'])!,
|
||||
migration: QueueResponseDto.fromJson(json[r'migration'])!,
|
||||
notifications: QueueResponseDto.fromJson(json[r'notifications'])!,
|
||||
ocr: QueueResponseDto.fromJson(json[r'ocr'])!,
|
||||
search: QueueResponseDto.fromJson(json[r'search'])!,
|
||||
sidecar: QueueResponseDto.fromJson(json[r'sidecar'])!,
|
||||
smartSearch: QueueResponseDto.fromJson(json[r'smartSearch'])!,
|
||||
storageTemplateMigration: QueueResponseDto.fromJson(json[r'storageTemplateMigration'])!,
|
||||
thumbnailGeneration: QueueResponseDto.fromJson(json[r'thumbnailGeneration'])!,
|
||||
videoConversion: QueueResponseDto.fromJson(json[r'videoConversion'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AllJobStatusResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AllJobStatusResponseDto>[];
|
||||
static List<QueuesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <QueuesResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AllJobStatusResponseDto.fromJson(row);
|
||||
final value = QueuesResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
@ -169,12 +169,12 @@ class AllJobStatusResponseDto {
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AllJobStatusResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AllJobStatusResponseDto>{};
|
||||
static Map<String, QueuesResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, QueuesResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AllJobStatusResponseDto.fromJson(entry.value);
|
||||
final value = QueuesResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
@ -183,14 +183,14 @@ class AllJobStatusResponseDto {
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AllJobStatusResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AllJobStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AllJobStatusResponseDto>>{};
|
||||
// maps a json object with a list of QueuesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<QueuesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<QueuesResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AllJobStatusResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
map[entry.key] = QueuesResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
@ -4836,14 +4836,14 @@
|
||||
"/jobs": {
|
||||
"get": {
|
||||
"description": "Retrieve the counts of the current queue, as well as the current status.",
|
||||
"operationId": "getAllJobsStatus",
|
||||
"operationId": "getQueuesLegacy",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AllJobStatusResponseDto"
|
||||
"$ref": "#/components/schemas/QueuesResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4936,17 +4936,17 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/jobs/{id}": {
|
||||
"/jobs/{name}": {
|
||||
"put": {
|
||||
"description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.",
|
||||
"operationId": "sendJobCommand",
|
||||
"operationId": "runQueueCommandLegacy",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/JobName"
|
||||
"$ref": "#/components/schemas/QueueName"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -4954,7 +4954,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/JobCommandDto"
|
||||
"$ref": "#/components/schemas/QueueCommandDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -4965,7 +4965,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -14084,77 +14084,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"AllJobStatusResponseDto": {
|
||||
"properties": {
|
||||
"backgroundTask": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"backupDatabase": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"duplicateDetection": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"faceDetection": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"facialRecognition": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"library": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"metadataExtraction": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"migration": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"notifications": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"ocr": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"search": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"sidecar": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"smartSearch": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"storageTemplateMigration": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"thumbnailGeneration": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
},
|
||||
"videoConversion": {
|
||||
"$ref": "#/components/schemas/JobStatusDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"backgroundTask",
|
||||
"backupDatabase",
|
||||
"duplicateDetection",
|
||||
"faceDetection",
|
||||
"facialRecognition",
|
||||
"library",
|
||||
"metadataExtraction",
|
||||
"migration",
|
||||
"notifications",
|
||||
"ocr",
|
||||
"search",
|
||||
"sidecar",
|
||||
"smartSearch",
|
||||
"storageTemplateMigration",
|
||||
"thumbnailGeneration",
|
||||
"videoConversion"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetBulkDeleteDto": {
|
||||
"properties": {
|
||||
"force": {
|
||||
@ -15866,65 +15795,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"JobCommand": {
|
||||
"enum": [
|
||||
"start",
|
||||
"pause",
|
||||
"resume",
|
||||
"empty",
|
||||
"clear-failed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"JobCommandDto": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/JobCommand"
|
||||
}
|
||||
]
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JobCountsDto": {
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "integer"
|
||||
},
|
||||
"completed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"delayed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"paused": {
|
||||
"type": "integer"
|
||||
},
|
||||
"waiting": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"active",
|
||||
"completed",
|
||||
"delayed",
|
||||
"failed",
|
||||
"paused",
|
||||
"waiting"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JobCreateDto": {
|
||||
"properties": {
|
||||
"name": {
|
||||
@ -15940,27 +15810,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JobName": {
|
||||
"enum": [
|
||||
"thumbnailGeneration",
|
||||
"metadataExtraction",
|
||||
"videoConversion",
|
||||
"faceDetection",
|
||||
"facialRecognition",
|
||||
"smartSearch",
|
||||
"duplicateDetection",
|
||||
"backgroundTask",
|
||||
"storageTemplateMigration",
|
||||
"migration",
|
||||
"search",
|
||||
"sidecar",
|
||||
"library",
|
||||
"notifications",
|
||||
"backupDatabase",
|
||||
"ocr"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"JobSettingsDto": {
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
@ -15973,21 +15822,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"JobStatusDto": {
|
||||
"properties": {
|
||||
"jobCounts": {
|
||||
"$ref": "#/components/schemas/JobCountsDto"
|
||||
},
|
||||
"queueStatus": {
|
||||
"$ref": "#/components/schemas/QueueStatusDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jobCounts",
|
||||
"queueStatus"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LibraryResponseDto": {
|
||||
"properties": {
|
||||
"assetCount": {
|
||||
@ -17559,6 +17393,101 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"QueueCommand": {
|
||||
"enum": [
|
||||
"start",
|
||||
"pause",
|
||||
"resume",
|
||||
"empty",
|
||||
"clear-failed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"QueueCommandDto": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QueueCommand"
|
||||
}
|
||||
]
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueName": {
|
||||
"enum": [
|
||||
"thumbnailGeneration",
|
||||
"metadataExtraction",
|
||||
"videoConversion",
|
||||
"faceDetection",
|
||||
"facialRecognition",
|
||||
"smartSearch",
|
||||
"duplicateDetection",
|
||||
"backgroundTask",
|
||||
"storageTemplateMigration",
|
||||
"migration",
|
||||
"search",
|
||||
"sidecar",
|
||||
"library",
|
||||
"notifications",
|
||||
"backupDatabase",
|
||||
"ocr"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"QueueResponseDto": {
|
||||
"properties": {
|
||||
"jobCounts": {
|
||||
"$ref": "#/components/schemas/QueueStatisticsDto"
|
||||
},
|
||||
"queueStatus": {
|
||||
"$ref": "#/components/schemas/QueueStatusDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jobCounts",
|
||||
"queueStatus"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatisticsDto": {
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "integer"
|
||||
},
|
||||
"completed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"delayed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"paused": {
|
||||
"type": "integer"
|
||||
},
|
||||
"waiting": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"active",
|
||||
"completed",
|
||||
"delayed",
|
||||
"failed",
|
||||
"paused",
|
||||
"waiting"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatusDto": {
|
||||
"properties": {
|
||||
"isActive": {
|
||||
@ -17574,6 +17503,77 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueuesResponseDto": {
|
||||
"properties": {
|
||||
"backgroundTask": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"backupDatabase": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"duplicateDetection": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"faceDetection": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"facialRecognition": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"library": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"metadataExtraction": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"migration": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"notifications": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"ocr": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"search": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"sidecar": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"smartSearch": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"storageTemplateMigration": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"thumbnailGeneration": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
},
|
||||
"videoConversion": {
|
||||
"$ref": "#/components/schemas/QueueResponseDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"backgroundTask",
|
||||
"backupDatabase",
|
||||
"duplicateDetection",
|
||||
"faceDetection",
|
||||
"facialRecognition",
|
||||
"library",
|
||||
"metadataExtraction",
|
||||
"migration",
|
||||
"notifications",
|
||||
"ocr",
|
||||
"search",
|
||||
"sidecar",
|
||||
"smartSearch",
|
||||
"storageTemplateMigration",
|
||||
"thumbnailGeneration",
|
||||
"videoConversion"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RandomSearchDto": {
|
||||
"properties": {
|
||||
"albumIds": {
|
||||
|
||||
@ -699,7 +699,7 @@ export type AssetFaceDeleteDto = {
|
||||
export type FaceDto = {
|
||||
id: string;
|
||||
};
|
||||
export type JobCountsDto = {
|
||||
export type QueueStatisticsDto = {
|
||||
active: number;
|
||||
completed: number;
|
||||
delayed: number;
|
||||
@ -711,33 +711,33 @@ export type QueueStatusDto = {
|
||||
isActive: boolean;
|
||||
isPaused: boolean;
|
||||
};
|
||||
export type JobStatusDto = {
|
||||
jobCounts: JobCountsDto;
|
||||
export type QueueResponseDto = {
|
||||
jobCounts: QueueStatisticsDto;
|
||||
queueStatus: QueueStatusDto;
|
||||
};
|
||||
export type AllJobStatusResponseDto = {
|
||||
backgroundTask: JobStatusDto;
|
||||
backupDatabase: JobStatusDto;
|
||||
duplicateDetection: JobStatusDto;
|
||||
faceDetection: JobStatusDto;
|
||||
facialRecognition: JobStatusDto;
|
||||
library: JobStatusDto;
|
||||
metadataExtraction: JobStatusDto;
|
||||
migration: JobStatusDto;
|
||||
notifications: JobStatusDto;
|
||||
ocr: JobStatusDto;
|
||||
search: JobStatusDto;
|
||||
sidecar: JobStatusDto;
|
||||
smartSearch: JobStatusDto;
|
||||
storageTemplateMigration: JobStatusDto;
|
||||
thumbnailGeneration: JobStatusDto;
|
||||
videoConversion: JobStatusDto;
|
||||
export type QueuesResponseDto = {
|
||||
backgroundTask: QueueResponseDto;
|
||||
backupDatabase: QueueResponseDto;
|
||||
duplicateDetection: QueueResponseDto;
|
||||
faceDetection: QueueResponseDto;
|
||||
facialRecognition: QueueResponseDto;
|
||||
library: QueueResponseDto;
|
||||
metadataExtraction: QueueResponseDto;
|
||||
migration: QueueResponseDto;
|
||||
notifications: QueueResponseDto;
|
||||
ocr: QueueResponseDto;
|
||||
search: QueueResponseDto;
|
||||
sidecar: QueueResponseDto;
|
||||
smartSearch: QueueResponseDto;
|
||||
storageTemplateMigration: QueueResponseDto;
|
||||
thumbnailGeneration: QueueResponseDto;
|
||||
videoConversion: QueueResponseDto;
|
||||
};
|
||||
export type JobCreateDto = {
|
||||
name: ManualJobName;
|
||||
};
|
||||
export type JobCommandDto = {
|
||||
command: JobCommand;
|
||||
export type QueueCommandDto = {
|
||||
command: QueueCommand;
|
||||
force?: boolean;
|
||||
};
|
||||
export type LibraryResponseDto = {
|
||||
@ -2805,10 +2805,10 @@ export function reassignFacesById({ id, faceDto }: {
|
||||
/**
|
||||
* Retrieve queue counts and status
|
||||
*/
|
||||
export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) {
|
||||
export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AllJobStatusResponseDto;
|
||||
data: QueuesResponseDto;
|
||||
}>("/jobs", {
|
||||
...opts
|
||||
}));
|
||||
@ -2828,17 +2828,17 @@ export function createJob({ jobCreateDto }: {
|
||||
/**
|
||||
* Run jobs
|
||||
*/
|
||||
export function sendJobCommand({ id, jobCommandDto }: {
|
||||
id: JobName;
|
||||
jobCommandDto: JobCommandDto;
|
||||
export function runQueueCommandLegacy({ name, queueCommandDto }: {
|
||||
name: QueueName;
|
||||
queueCommandDto: QueueCommandDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: JobStatusDto;
|
||||
}>(`/jobs/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
data: QueueResponseDto;
|
||||
}>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PUT",
|
||||
body: jobCommandDto
|
||||
body: queueCommandDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
@ -5067,7 +5067,7 @@ export enum ManualJobName {
|
||||
MemoryCreate = "memory-create",
|
||||
BackupDatabase = "backup-database"
|
||||
}
|
||||
export enum JobName {
|
||||
export enum QueueName {
|
||||
ThumbnailGeneration = "thumbnailGeneration",
|
||||
MetadataExtraction = "metadataExtraction",
|
||||
VideoConversion = "videoConversion",
|
||||
@ -5085,7 +5085,7 @@ export enum JobName {
|
||||
BackupDatabase = "backupDatabase",
|
||||
Ocr = "ocr"
|
||||
}
|
||||
export enum JobCommand {
|
||||
export enum QueueCommand {
|
||||
Start = "start",
|
||||
Pause = "pause",
|
||||
Resume = "resume",
|
||||
|
||||
@ -23,7 +23,7 @@ import { WebsocketRepository } from 'src/repositories/websocket.repository';
|
||||
import { services } from 'src/services';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
import { getKyselyConfig } from 'src/utils/database';
|
||||
|
||||
const common = [...repositories, ...services, GlobalExceptionFilter];
|
||||
@ -52,11 +52,11 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(
|
||||
@Inject(IWorker) private worker: ImmichWorker,
|
||||
logger: LoggingRepository,
|
||||
private eventRepository: EventRepository,
|
||||
private websocketRepository: WebsocketRepository,
|
||||
private jobService: JobService,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
private authService: AuthService,
|
||||
private eventRepository: EventRepository,
|
||||
private queueService: QueueService,
|
||||
private telemetryRepository: TelemetryRepository,
|
||||
private websocketRepository: WebsocketRepository,
|
||||
) {
|
||||
logger.setAppName(this.worker);
|
||||
}
|
||||
@ -64,7 +64,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
async onModuleInit() {
|
||||
this.telemetryRepository.setup({ repositories });
|
||||
|
||||
this.jobService.setServices(services);
|
||||
this.queueService.setServices(services);
|
||||
|
||||
this.websocketRepository.setAuthFn(async (client) =>
|
||||
this.authService.authenticate({
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import { JobCreateDto } from 'src/dtos/job.dto';
|
||||
import { QueueCommandDto, QueueNameParamDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto';
|
||||
import { ApiTag, Permission } from 'src/enum';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
|
||||
@ApiTags(ApiTag.Jobs)
|
||||
@Controller('jobs')
|
||||
export class JobController {
|
||||
constructor(private service: JobService) {}
|
||||
constructor(
|
||||
private service: JobService,
|
||||
private queueService: QueueService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.JobRead, admin: true })
|
||||
@ -18,8 +23,8 @@ export class JobController {
|
||||
description: 'Retrieve the counts of the current queue, as well as the current status.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
|
||||
return this.service.getAllJobsStatus();
|
||||
getQueuesLegacy(): Promise<QueuesResponseDto> {
|
||||
return this.queueService.getAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ -35,7 +40,7 @@ export class JobController {
|
||||
return this.service.create(dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@Put(':name')
|
||||
@Authenticated({ permission: Permission.JobCreate, admin: true })
|
||||
@Endpoint({
|
||||
summary: 'Run jobs',
|
||||
@ -43,7 +48,7 @@ export class JobController {
|
||||
'Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
|
||||
return this.service.handleCommand(id, dto);
|
||||
runQueueCommandLegacy(@Param() { name }: QueueNameParamDto, @Body() dto: QueueCommandDto): Promise<QueueResponseDto> {
|
||||
return this.queueService.runCommand(name, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,99 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { JobCommand, ManualJobName, QueueName } from 'src/enum';
|
||||
import { ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||
|
||||
export class JobIdParamDto {
|
||||
@ValidateEnum({ enum: QueueName, name: 'JobName' })
|
||||
id!: QueueName;
|
||||
}
|
||||
|
||||
export class JobCommandDto {
|
||||
@ValidateEnum({ enum: JobCommand, name: 'JobCommand' })
|
||||
command!: JobCommand;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
|
||||
}
|
||||
import { ManualJobName } from 'src/enum';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
export class JobCreateDto {
|
||||
@ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' })
|
||||
name!: ManualJobName;
|
||||
}
|
||||
|
||||
export class JobCountsDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
active!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
completed!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
failed!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
delayed!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
waiting!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
paused!: number;
|
||||
}
|
||||
|
||||
export class QueueStatusDto {
|
||||
isActive!: boolean;
|
||||
isPaused!: boolean;
|
||||
}
|
||||
|
||||
export class JobStatusDto {
|
||||
@ApiProperty({ type: JobCountsDto })
|
||||
jobCounts!: JobCountsDto;
|
||||
|
||||
@ApiProperty({ type: QueueStatusDto })
|
||||
queueStatus!: QueueStatusDto;
|
||||
}
|
||||
|
||||
export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto> {
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.ThumbnailGeneration]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.MetadataExtraction]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.VideoConversion]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.SmartSearch]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.StorageTemplateMigration]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.Migration]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.BackgroundTask]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.Search]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.DuplicateDetection]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.FaceDetection]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.FacialRecognition]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.Sidecar]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.Library]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.Notification]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.BackupDatabase]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.Ocr]!: JobStatusDto;
|
||||
}
|
||||
|
||||
94
server/src/dtos/queue.dto.ts
Normal file
94
server/src/dtos/queue.dto.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { QueueCommand, QueueName } from 'src/enum';
|
||||
import { ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||
|
||||
export class QueueNameParamDto {
|
||||
@ValidateEnum({ enum: QueueName, name: 'QueueName' })
|
||||
name!: QueueName;
|
||||
}
|
||||
|
||||
export class QueueCommandDto {
|
||||
@ValidateEnum({ enum: QueueCommand, name: 'QueueCommand' })
|
||||
command!: QueueCommand;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
|
||||
}
|
||||
|
||||
export class QueueStatisticsDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
active!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
completed!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
failed!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
delayed!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
waiting!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
paused!: number;
|
||||
}
|
||||
|
||||
export class QueueStatusDto {
|
||||
isActive!: boolean;
|
||||
isPaused!: boolean;
|
||||
}
|
||||
|
||||
export class QueueResponseDto {
|
||||
@ApiProperty({ type: QueueStatisticsDto })
|
||||
jobCounts!: QueueStatisticsDto;
|
||||
|
||||
@ApiProperty({ type: QueueStatusDto })
|
||||
queueStatus!: QueueStatusDto;
|
||||
}
|
||||
|
||||
export class QueuesResponseDto implements Record<QueueName, QueueResponseDto> {
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.ThumbnailGeneration]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.MetadataExtraction]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.VideoConversion]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.SmartSearch]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.StorageTemplateMigration]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Migration]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.BackgroundTask]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Search]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.DuplicateDetection]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.FaceDetection]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.FacialRecognition]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Sidecar]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Library]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Notification]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.BackupDatabase]!: QueueResponseDto;
|
||||
|
||||
@ApiProperty({ type: QueueResponseDto })
|
||||
[QueueName.Ocr]!: QueueResponseDto;
|
||||
}
|
||||
@ -603,7 +603,7 @@ export enum JobName {
|
||||
Ocr = 'Ocr',
|
||||
}
|
||||
|
||||
export enum JobCommand {
|
||||
export enum QueueCommand {
|
||||
Start = 'start',
|
||||
Pause = 'pause',
|
||||
Resume = 'resume',
|
||||
|
||||
@ -7,7 +7,6 @@ import { ONE_HOUR } from 'src/constants';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { OpenGraphTags } from 'src/utils/misc';
|
||||
@ -40,7 +39,6 @@ const render = (index: string, meta: OpenGraphTags) => {
|
||||
export class ApiService {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private jobService: JobService,
|
||||
private sharedLinkService: SharedLinkService,
|
||||
private versionService: VersionService,
|
||||
private configRepository: ConfigRepository,
|
||||
|
||||
@ -23,6 +23,7 @@ import { NotificationService } from 'src/services/notification.service';
|
||||
import { OcrService } from 'src/services/ocr.service';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
import { SearchService } from 'src/services/search.service';
|
||||
import { ServerService } from 'src/services/server.service';
|
||||
import { SessionService } from 'src/services/session.service';
|
||||
@ -69,6 +70,7 @@ export const services = [
|
||||
OcrService,
|
||||
PartnerService,
|
||||
PersonService,
|
||||
QueueService,
|
||||
SearchService,
|
||||
ServerService,
|
||||
SessionService,
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ImmichWorker, JobCommand, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { JobItem } from 'src/types';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@ -20,209 +18,6 @@ describe(JobService.name, () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('onConfigUpdate', () => {
|
||||
it('should update concurrency', () => {
|
||||
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
|
||||
|
||||
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(16);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.StorageTemplateMigration, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleNightlyJobs', () => {
|
||||
it('should run the scheduled jobs', async () => {
|
||||
await sut.handleNightlyJobs();
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.AssetDeleteCheck },
|
||||
{ name: JobName.UserDeleteCheck },
|
||||
{ name: JobName.PersonCleanup },
|
||||
{ name: JobName.MemoryCleanup },
|
||||
{ name: JobName.SessionCleanup },
|
||||
{ name: JobName.AuditTableCleanup },
|
||||
{ name: JobName.AuditLogCleanup },
|
||||
{ name: JobName.MemoryGenerate },
|
||||
{ name: JobName.UserSyncUsage },
|
||||
{ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } },
|
||||
{ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllJobStatus', () => {
|
||||
it('should get all job statuses', async () => {
|
||||
mocks.job.getJobCounts.mockResolvedValue({
|
||||
active: 1,
|
||||
completed: 1,
|
||||
failed: 1,
|
||||
delayed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
});
|
||||
mocks.job.getQueueStatus.mockResolvedValue({
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
});
|
||||
|
||||
const expectedJobStatus = {
|
||||
jobCounts: {
|
||||
active: 1,
|
||||
completed: 1,
|
||||
delayed: 1,
|
||||
failed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
},
|
||||
queueStatus: {
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(sut.getAllJobsStatus()).resolves.toEqual({
|
||||
[QueueName.BackgroundTask]: expectedJobStatus,
|
||||
[QueueName.DuplicateDetection]: expectedJobStatus,
|
||||
[QueueName.SmartSearch]: expectedJobStatus,
|
||||
[QueueName.MetadataExtraction]: expectedJobStatus,
|
||||
[QueueName.Search]: expectedJobStatus,
|
||||
[QueueName.StorageTemplateMigration]: expectedJobStatus,
|
||||
[QueueName.Migration]: expectedJobStatus,
|
||||
[QueueName.ThumbnailGeneration]: expectedJobStatus,
|
||||
[QueueName.VideoConversion]: expectedJobStatus,
|
||||
[QueueName.FaceDetection]: expectedJobStatus,
|
||||
[QueueName.FacialRecognition]: expectedJobStatus,
|
||||
[QueueName.Sidecar]: expectedJobStatus,
|
||||
[QueueName.Library]: expectedJobStatus,
|
||||
[QueueName.Notification]: expectedJobStatus,
|
||||
[QueueName.BackupDatabase]: expectedJobStatus,
|
||||
[QueueName.Ocr]: expectedJobStatus,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCommand', () => {
|
||||
it('should handle a pause command', async () => {
|
||||
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Pause, force: false });
|
||||
|
||||
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should handle a resume command', async () => {
|
||||
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Resume, force: false });
|
||||
|
||||
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should handle an empty command', async () => {
|
||||
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Empty, force: false });
|
||||
|
||||
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should not start a job that is already running', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
|
||||
|
||||
await expect(
|
||||
sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a start video conversion command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start storage template migration command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.StorageTemplateMigration, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
|
||||
});
|
||||
|
||||
it('should handle a start smart search command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.SmartSearch, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start metadata extraction command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetExtractMetadataQueueAll,
|
||||
data: { force: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a start sidecar command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.Sidecar, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start thumbnail generation command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.ThumbnailGeneration, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetGenerateThumbnailsQueueAll,
|
||||
data: { force: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a start face detection command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.FaceDetection, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start facial recognition command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.FacialRecognition, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start backup database command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.handleCommand(QueueName.BackupDatabase, { command: JobCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should throw a bad request when an invalid queue is used', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await expect(
|
||||
sut.handleCommand(QueueName.BackgroundTask, { command: JobCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onJobRun', () => {
|
||||
it('should process a successful job', async () => {
|
||||
mocks.job.run.mockResolvedValue(JobStatus.Success);
|
||||
|
||||
@ -1,28 +1,12 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import {
|
||||
AssetType,
|
||||
AssetVisibility,
|
||||
BootstrapEventPriority,
|
||||
CronJob,
|
||||
DatabaseLock,
|
||||
ImmichWorker,
|
||||
JobCommand,
|
||||
JobName,
|
||||
JobStatus,
|
||||
ManualJobName,
|
||||
QueueCleanType,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { ArgOf, ArgsOf } from 'src/repositories/event.repository';
|
||||
import { JobCreateDto } from 'src/dtos/job.dto';
|
||||
import { AssetType, AssetVisibility, JobName, JobStatus, ManualJobName } from 'src/enum';
|
||||
import { ArgsOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { ConcurrentQueueName, JobItem } from 'src/types';
|
||||
import { JobItem } from 'src/types';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
const asJobItem = (dto: JobCreateDto): JobItem => {
|
||||
switch (dto.name) {
|
||||
@ -56,196 +40,12 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
||||
}
|
||||
};
|
||||
|
||||
const asNightlyTasksCron = (config: SystemConfig) => {
|
||||
const [hours, minutes] = config.nightlyTasks.startTime.split(':').map(Number);
|
||||
return `${minutes} ${hours} * * *`;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class JobService extends BaseService {
|
||||
private services: ClassConstructor<unknown>[] = [];
|
||||
private nightlyJobsLock = false;
|
||||
|
||||
@OnEvent({ name: 'ConfigInit' })
|
||||
async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) {
|
||||
if (this.worker === ImmichWorker.Microservices) {
|
||||
this.updateQueueConcurrency(config);
|
||||
return;
|
||||
}
|
||||
|
||||
this.nightlyJobsLock = await this.databaseRepository.tryLock(DatabaseLock.NightlyJobs);
|
||||
if (this.nightlyJobsLock) {
|
||||
const cronExpression = asNightlyTasksCron(config);
|
||||
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
|
||||
this.cronRepository.create({
|
||||
name: CronJob.NightlyJobs,
|
||||
expression: cronExpression,
|
||||
start: true,
|
||||
onTick: () => handlePromiseError(this.handleNightlyJobs(), this.logger),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) {
|
||||
if (this.worker === ImmichWorker.Microservices) {
|
||||
this.updateQueueConcurrency(config);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.nightlyJobsLock) {
|
||||
const cronExpression = asNightlyTasksCron(config);
|
||||
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
|
||||
this.cronRepository.update({ name: CronJob.NightlyJobs, expression: cronExpression, start: true });
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService })
|
||||
onBootstrap() {
|
||||
this.jobRepository.setup(this.services);
|
||||
if (this.worker === ImmichWorker.Microservices) {
|
||||
this.jobRepository.startWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
private updateQueueConcurrency(config: SystemConfig) {
|
||||
this.logger.debug(`Updating queue concurrency settings`);
|
||||
for (const queueName of Object.values(QueueName)) {
|
||||
let concurrency = 1;
|
||||
if (this.isConcurrentQueue(queueName)) {
|
||||
concurrency = config.job[queueName].concurrency;
|
||||
}
|
||||
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
|
||||
this.jobRepository.setConcurrency(queueName, concurrency);
|
||||
}
|
||||
}
|
||||
|
||||
setServices(services: ClassConstructor<unknown>[]) {
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
async create(dto: JobCreateDto): Promise<void> {
|
||||
await this.jobRepository.queue(asJobItem(dto));
|
||||
}
|
||||
|
||||
async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise<JobStatusDto> {
|
||||
this.logger.debug(`Handling command: queue=${queueName},command=${dto.command},force=${dto.force}`);
|
||||
|
||||
switch (dto.command) {
|
||||
case JobCommand.Start: {
|
||||
await this.start(queueName, dto);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.Pause: {
|
||||
await this.jobRepository.pause(queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.Resume: {
|
||||
await this.jobRepository.resume(queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.Empty: {
|
||||
await this.jobRepository.empty(queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.ClearFailed: {
|
||||
const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.Failed);
|
||||
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getJobStatus(queueName);
|
||||
}
|
||||
|
||||
async getJobStatus(queueName: QueueName): Promise<JobStatusDto> {
|
||||
const [jobCounts, queueStatus] = await Promise.all([
|
||||
this.jobRepository.getJobCounts(queueName),
|
||||
this.jobRepository.getQueueStatus(queueName),
|
||||
]);
|
||||
|
||||
return { jobCounts, queueStatus };
|
||||
}
|
||||
|
||||
async getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
|
||||
const response = new AllJobStatusResponseDto();
|
||||
for (const queueName of Object.values(QueueName)) {
|
||||
response[queueName] = await this.getJobStatus(queueName);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private async start(name: QueueName, { force }: JobCommandDto): Promise<void> {
|
||||
const { isActive } = await this.jobRepository.getQueueStatus(name);
|
||||
if (isActive) {
|
||||
throw new BadRequestException(`Job is already running`);
|
||||
}
|
||||
|
||||
await this.eventRepository.emit('QueueStart', { name });
|
||||
|
||||
switch (name) {
|
||||
case QueueName.VideoConversion: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.StorageTemplateMigration: {
|
||||
return this.jobRepository.queue({ name: JobName.StorageTemplateMigration });
|
||||
}
|
||||
|
||||
case QueueName.Migration: {
|
||||
return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll });
|
||||
}
|
||||
|
||||
case QueueName.SmartSearch: {
|
||||
return this.jobRepository.queue({ name: JobName.SmartSearchQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.DuplicateDetection: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetDetectDuplicatesQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.MetadataExtraction: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetExtractMetadataQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.Sidecar: {
|
||||
return this.jobRepository.queue({ name: JobName.SidecarQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.ThumbnailGeneration: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FaceDetection: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetDetectFacesQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FacialRecognition: {
|
||||
return this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.Library: {
|
||||
return this.jobRepository.queue({ name: JobName.LibraryScanQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.BackupDatabase: {
|
||||
return this.jobRepository.queue({ name: JobName.DatabaseBackup, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.Ocr: {
|
||||
return this.jobRepository.queue({ name: JobName.OcrQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestException(`Invalid job name: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobRun' })
|
||||
async onJobRun(...[queueName, job]: ArgsOf<'JobRun'>) {
|
||||
try {
|
||||
@ -262,50 +62,6 @@ export class JobService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private isConcurrentQueue(name: QueueName): name is ConcurrentQueueName {
|
||||
return ![
|
||||
QueueName.FacialRecognition,
|
||||
QueueName.StorageTemplateMigration,
|
||||
QueueName.DuplicateDetection,
|
||||
QueueName.BackupDatabase,
|
||||
].includes(name);
|
||||
}
|
||||
|
||||
async handleNightlyJobs() {
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
const jobs: JobItem[] = [];
|
||||
|
||||
if (config.nightlyTasks.databaseCleanup) {
|
||||
jobs.push(
|
||||
{ name: JobName.AssetDeleteCheck },
|
||||
{ name: JobName.UserDeleteCheck },
|
||||
{ name: JobName.PersonCleanup },
|
||||
{ name: JobName.MemoryCleanup },
|
||||
{ name: JobName.SessionCleanup },
|
||||
{ name: JobName.AuditTableCleanup },
|
||||
{ name: JobName.AuditLogCleanup },
|
||||
);
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.generateMemories) {
|
||||
jobs.push({ name: JobName.MemoryGenerate });
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.syncQuotaUsage) {
|
||||
jobs.push({ name: JobName.UserSyncUsage });
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.missingThumbnails) {
|
||||
jobs.push({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } });
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.clusterNewFaces) {
|
||||
jobs.push({ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } });
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue follow up jobs
|
||||
*/
|
||||
|
||||
223
server/src/services/queue.service.spec.ts
Normal file
223
server/src/services/queue.service.spec.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
|
||||
import { QueueService } from 'src/services/queue.service';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
describe(QueueService.name, () => {
|
||||
let sut: QueueService;
|
||||
let mocks: ServiceMocks;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, mocks } = newTestService(QueueService));
|
||||
|
||||
mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('onConfigUpdate', () => {
|
||||
it('should update concurrency', () => {
|
||||
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
|
||||
|
||||
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(16);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5);
|
||||
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.StorageTemplateMigration, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleNightlyJobs', () => {
|
||||
it('should run the scheduled jobs', async () => {
|
||||
await sut.handleNightlyJobs();
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.AssetDeleteCheck },
|
||||
{ name: JobName.UserDeleteCheck },
|
||||
{ name: JobName.PersonCleanup },
|
||||
{ name: JobName.MemoryCleanup },
|
||||
{ name: JobName.SessionCleanup },
|
||||
{ name: JobName.AuditTableCleanup },
|
||||
{ name: JobName.AuditLogCleanup },
|
||||
{ name: JobName.MemoryGenerate },
|
||||
{ name: JobName.UserSyncUsage },
|
||||
{ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } },
|
||||
{ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllJobStatus', () => {
|
||||
it('should get all job statuses', async () => {
|
||||
mocks.job.getJobCounts.mockResolvedValue({
|
||||
active: 1,
|
||||
completed: 1,
|
||||
failed: 1,
|
||||
delayed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
});
|
||||
mocks.job.getQueueStatus.mockResolvedValue({
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
});
|
||||
|
||||
const expectedJobStatus = {
|
||||
jobCounts: {
|
||||
active: 1,
|
||||
completed: 1,
|
||||
delayed: 1,
|
||||
failed: 1,
|
||||
waiting: 1,
|
||||
paused: 1,
|
||||
},
|
||||
queueStatus: {
|
||||
isActive: true,
|
||||
isPaused: true,
|
||||
},
|
||||
};
|
||||
|
||||
await expect(sut.getAll()).resolves.toEqual({
|
||||
[QueueName.BackgroundTask]: expectedJobStatus,
|
||||
[QueueName.DuplicateDetection]: expectedJobStatus,
|
||||
[QueueName.SmartSearch]: expectedJobStatus,
|
||||
[QueueName.MetadataExtraction]: expectedJobStatus,
|
||||
[QueueName.Search]: expectedJobStatus,
|
||||
[QueueName.StorageTemplateMigration]: expectedJobStatus,
|
||||
[QueueName.Migration]: expectedJobStatus,
|
||||
[QueueName.ThumbnailGeneration]: expectedJobStatus,
|
||||
[QueueName.VideoConversion]: expectedJobStatus,
|
||||
[QueueName.FaceDetection]: expectedJobStatus,
|
||||
[QueueName.FacialRecognition]: expectedJobStatus,
|
||||
[QueueName.Sidecar]: expectedJobStatus,
|
||||
[QueueName.Library]: expectedJobStatus,
|
||||
[QueueName.Notification]: expectedJobStatus,
|
||||
[QueueName.BackupDatabase]: expectedJobStatus,
|
||||
[QueueName.Ocr]: expectedJobStatus,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCommand', () => {
|
||||
it('should handle a pause command', async () => {
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false });
|
||||
|
||||
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should handle a resume command', async () => {
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false });
|
||||
|
||||
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should handle an empty command', async () => {
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false });
|
||||
|
||||
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||
});
|
||||
|
||||
it('should not start a job that is already running', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
|
||||
|
||||
await expect(
|
||||
sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a start video conversion command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start storage template migration command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
|
||||
});
|
||||
|
||||
it('should handle a start smart search command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.SmartSearch, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start metadata extraction command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetExtractMetadataQueueAll,
|
||||
data: { force: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a start sidecar command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.Sidecar, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start thumbnail generation command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.AssetGenerateThumbnailsQueueAll,
|
||||
data: { force: false },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle a start face detection command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.FaceDetection, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start facial recognition command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should handle a start backup database command', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await sut.runCommand(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false });
|
||||
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
|
||||
});
|
||||
|
||||
it('should throw a bad request when an invalid queue is used', async () => {
|
||||
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await expect(
|
||||
sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
250
server/src/services/queue.service.ts
Normal file
250
server/src/services/queue.service.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { QueueCommandDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto';
|
||||
import {
|
||||
BootstrapEventPriority,
|
||||
CronJob,
|
||||
DatabaseLock,
|
||||
ImmichWorker,
|
||||
JobName,
|
||||
QueueCleanType,
|
||||
QueueCommand,
|
||||
QueueName,
|
||||
} from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { ConcurrentQueueName, JobItem } from 'src/types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
const asNightlyTasksCron = (config: SystemConfig) => {
|
||||
const [hours, minutes] = config.nightlyTasks.startTime.split(':').map(Number);
|
||||
return `${minutes} ${hours} * * *`;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QueueService extends BaseService {
|
||||
private services: ClassConstructor<unknown>[] = [];
|
||||
private nightlyJobsLock = false;
|
||||
|
||||
@OnEvent({ name: 'ConfigInit' })
|
||||
async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) {
|
||||
if (this.worker === ImmichWorker.Microservices) {
|
||||
this.updateConcurrency(config);
|
||||
return;
|
||||
}
|
||||
|
||||
this.nightlyJobsLock = await this.databaseRepository.tryLock(DatabaseLock.NightlyJobs);
|
||||
if (this.nightlyJobsLock) {
|
||||
const cronExpression = asNightlyTasksCron(config);
|
||||
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
|
||||
this.cronRepository.create({
|
||||
name: CronJob.NightlyJobs,
|
||||
expression: cronExpression,
|
||||
start: true,
|
||||
onTick: () => handlePromiseError(this.handleNightlyJobs(), this.logger),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) {
|
||||
if (this.worker === ImmichWorker.Microservices) {
|
||||
this.updateConcurrency(config);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.nightlyJobsLock) {
|
||||
const cronExpression = asNightlyTasksCron(config);
|
||||
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
|
||||
this.cronRepository.update({ name: CronJob.NightlyJobs, expression: cronExpression, start: true });
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService })
|
||||
onBootstrap() {
|
||||
this.jobRepository.setup(this.services);
|
||||
if (this.worker === ImmichWorker.Microservices) {
|
||||
this.jobRepository.startWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
private updateConcurrency(config: SystemConfig) {
|
||||
this.logger.debug(`Updating queue concurrency settings`);
|
||||
for (const queueName of Object.values(QueueName)) {
|
||||
let concurrency = 1;
|
||||
if (this.isConcurrentQueue(queueName)) {
|
||||
concurrency = config.job[queueName].concurrency;
|
||||
}
|
||||
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
|
||||
this.jobRepository.setConcurrency(queueName, concurrency);
|
||||
}
|
||||
}
|
||||
|
||||
setServices(services: ClassConstructor<unknown>[]) {
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
async runCommand(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseDto> {
|
||||
this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`);
|
||||
|
||||
switch (dto.command) {
|
||||
case QueueCommand.Start: {
|
||||
await this.start(name, dto);
|
||||
break;
|
||||
}
|
||||
|
||||
case QueueCommand.Pause: {
|
||||
await this.jobRepository.pause(name);
|
||||
break;
|
||||
}
|
||||
|
||||
case QueueCommand.Resume: {
|
||||
await this.jobRepository.resume(name);
|
||||
break;
|
||||
}
|
||||
|
||||
case QueueCommand.Empty: {
|
||||
await this.jobRepository.empty(name);
|
||||
break;
|
||||
}
|
||||
|
||||
case QueueCommand.ClearFailed: {
|
||||
const failedJobs = await this.jobRepository.clear(name, QueueCleanType.Failed);
|
||||
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getByName(name);
|
||||
}
|
||||
|
||||
async getAll(): Promise<QueuesResponseDto> {
|
||||
const response = new QueuesResponseDto();
|
||||
for (const name of Object.values(QueueName)) {
|
||||
response[name] = await this.getByName(name);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async getByName(name: QueueName): Promise<QueueResponseDto> {
|
||||
const [jobCounts, queueStatus] = await Promise.all([
|
||||
this.jobRepository.getJobCounts(name),
|
||||
this.jobRepository.getQueueStatus(name),
|
||||
]);
|
||||
|
||||
return { jobCounts, queueStatus };
|
||||
}
|
||||
|
||||
private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {
|
||||
const { isActive } = await this.jobRepository.getQueueStatus(name);
|
||||
if (isActive) {
|
||||
throw new BadRequestException(`Job is already running`);
|
||||
}
|
||||
|
||||
await this.eventRepository.emit('QueueStart', { name });
|
||||
|
||||
switch (name) {
|
||||
case QueueName.VideoConversion: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.StorageTemplateMigration: {
|
||||
return this.jobRepository.queue({ name: JobName.StorageTemplateMigration });
|
||||
}
|
||||
|
||||
case QueueName.Migration: {
|
||||
return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll });
|
||||
}
|
||||
|
||||
case QueueName.SmartSearch: {
|
||||
return this.jobRepository.queue({ name: JobName.SmartSearchQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.DuplicateDetection: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetDetectDuplicatesQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.MetadataExtraction: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetExtractMetadataQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.Sidecar: {
|
||||
return this.jobRepository.queue({ name: JobName.SidecarQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.ThumbnailGeneration: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FaceDetection: {
|
||||
return this.jobRepository.queue({ name: JobName.AssetDetectFacesQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FacialRecognition: {
|
||||
return this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.Library: {
|
||||
return this.jobRepository.queue({ name: JobName.LibraryScanQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.BackupDatabase: {
|
||||
return this.jobRepository.queue({ name: JobName.DatabaseBackup, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.Ocr: {
|
||||
return this.jobRepository.queue({ name: JobName.OcrQueueAll, data: { force } });
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new BadRequestException(`Invalid job name: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isConcurrentQueue(name: QueueName): name is ConcurrentQueueName {
|
||||
return ![
|
||||
QueueName.FacialRecognition,
|
||||
QueueName.StorageTemplateMigration,
|
||||
QueueName.DuplicateDetection,
|
||||
QueueName.BackupDatabase,
|
||||
].includes(name);
|
||||
}
|
||||
|
||||
async handleNightlyJobs() {
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
const jobs: JobItem[] = [];
|
||||
|
||||
if (config.nightlyTasks.databaseCleanup) {
|
||||
jobs.push(
|
||||
{ name: JobName.AssetDeleteCheck },
|
||||
{ name: JobName.UserDeleteCheck },
|
||||
{ name: JobName.PersonCleanup },
|
||||
{ name: JobName.MemoryCleanup },
|
||||
{ name: JobName.SessionCleanup },
|
||||
{ name: JobName.AuditTableCleanup },
|
||||
{ name: JobName.AuditLogCleanup },
|
||||
);
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.generateMemories) {
|
||||
jobs.push({ name: JobName.MemoryGenerate });
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.syncQuotaUsage) {
|
||||
jobs.push({ name: JobName.UserSyncUsage });
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.missingThumbnails) {
|
||||
jobs.push({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } });
|
||||
}
|
||||
|
||||
if (config.nightlyTasks.clusterNewFaces) {
|
||||
jobs.push({ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } });
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(jobs);
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,8 @@
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { getJobName } from '$lib/utils';
|
||||
import { JobName, type SystemConfigJobDto } from '@immich/sdk';
|
||||
import { getQueueName } from '$lib/utils';
|
||||
import { QueueName, type SystemConfigJobDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@ -13,18 +13,18 @@
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
|
||||
const jobNames = [
|
||||
JobName.ThumbnailGeneration,
|
||||
JobName.MetadataExtraction,
|
||||
JobName.Library,
|
||||
JobName.Sidecar,
|
||||
JobName.SmartSearch,
|
||||
JobName.FaceDetection,
|
||||
JobName.FacialRecognition,
|
||||
JobName.VideoConversion,
|
||||
JobName.StorageTemplateMigration,
|
||||
JobName.Migration,
|
||||
JobName.Ocr,
|
||||
const queueNames = [
|
||||
QueueName.ThumbnailGeneration,
|
||||
QueueName.MetadataExtraction,
|
||||
QueueName.Library,
|
||||
QueueName.Sidecar,
|
||||
QueueName.SmartSearch,
|
||||
QueueName.FaceDetection,
|
||||
QueueName.FacialRecognition,
|
||||
QueueName.VideoConversion,
|
||||
QueueName.StorageTemplateMigration,
|
||||
QueueName.Migration,
|
||||
QueueName.Ocr,
|
||||
];
|
||||
|
||||
function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto {
|
||||
@ -35,22 +35,22 @@
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
{#each jobNames as jobName (jobName)}
|
||||
{#each queueNames as queueName (queueName)}
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
{#if isSystemConfigJobDto(jobName)}
|
||||
{#if isSystemConfigJobDto(queueName)}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
bind:value={configToEdit.job[jobName].concurrency}
|
||||
bind:value={configToEdit.job[queueName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(configToEdit.job[jobName].concurrency == config.job[jobName].concurrency)}
|
||||
isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)}
|
||||
/>
|
||||
{:else}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
value={1}
|
||||
disabled={true}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/elements/Badge.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
|
||||
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusDto } from '@immich/sdk';
|
||||
import { Icon, IconButton } from '@immich/ui';
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
@ -22,21 +22,21 @@
|
||||
title: string;
|
||||
subtitle: string | undefined;
|
||||
description: Component | undefined;
|
||||
jobCounts: JobCountsDto;
|
||||
statistics: QueueStatisticsDto;
|
||||
queueStatus: QueueStatusDto;
|
||||
icon: string;
|
||||
disabled?: boolean;
|
||||
allText: string | undefined;
|
||||
refreshText: string | undefined;
|
||||
missingText: string;
|
||||
onCommand: (command: JobCommandDto) => void;
|
||||
onCommand: (command: QueueCommandDto) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
jobCounts,
|
||||
statistics,
|
||||
queueStatus,
|
||||
icon,
|
||||
disabled = false,
|
||||
@ -46,7 +46,7 @@
|
||||
onCommand,
|
||||
}: Props = $props();
|
||||
|
||||
let waitingCount = $derived(jobCounts.waiting + jobCounts.paused + jobCounts.delayed);
|
||||
let waitingCount = $derived(statistics.waiting + statistics.paused + statistics.delayed);
|
||||
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
|
||||
let multipleButtons = $derived(allText || refreshText);
|
||||
|
||||
@ -67,11 +67,11 @@
|
||||
<span class="uppercase">{title}</span>
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
{#if jobCounts.failed > 0}
|
||||
{#if statistics.failed > 0}
|
||||
<Badge>
|
||||
<div class="flex flex-row gap-1">
|
||||
<span class="text-sm">
|
||||
{$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })}
|
||||
{$t('admin.jobs_failed', { values: { jobCount: statistics.failed.toLocaleString($locale) } })}
|
||||
</span>
|
||||
<IconButton
|
||||
color="primary"
|
||||
@ -79,15 +79,15 @@
|
||||
aria-label={$t('clear_message')}
|
||||
size="tiny"
|
||||
shape="round"
|
||||
onclick={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
|
||||
onclick={() => onCommand({ command: QueueCommand.ClearFailed, force: false })}
|
||||
/>
|
||||
</div>
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if jobCounts.delayed > 0}
|
||||
{#if statistics.delayed > 0}
|
||||
<Badge>
|
||||
<span class="text-sm">
|
||||
{$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })}
|
||||
{$t('admin.jobs_delayed', { values: { jobCount: statistics.delayed.toLocaleString($locale) } })}
|
||||
</span>
|
||||
</Badge>
|
||||
{/if}
|
||||
@ -111,7 +111,7 @@
|
||||
>
|
||||
<p>{$t('active')}</p>
|
||||
<p class="text-2xl">
|
||||
{jobCounts.active.toLocaleString($locale)}
|
||||
{statistics.active.toLocaleString($locale)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -131,7 +131,7 @@
|
||||
<JobTileButton
|
||||
disabled={true}
|
||||
color="light-gray"
|
||||
onClick={() => onCommand({ command: JobCommand.Start, force: false })}
|
||||
onClick={() => onCommand({ command: QueueCommand.Start, force: false })}
|
||||
>
|
||||
<Icon icon={mdiAlertCircle} size="36" />
|
||||
<span class="uppercase">{$t('disabled')}</span>
|
||||
@ -140,20 +140,20 @@
|
||||
|
||||
{#if !disabled && !isIdle}
|
||||
{#if waitingCount > 0}
|
||||
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Empty, force: false })}>
|
||||
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}>
|
||||
<Icon icon={mdiClose} size="24" />
|
||||
<span class="uppercase">{$t('clear')}</span>
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
{#if queueStatus.isPaused}
|
||||
{@const size = waitingCount > 0 ? '24' : '48'}
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Resume, force: false })}>
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
|
||||
<!-- size property is not reactive, so have to use width and height -->
|
||||
<Icon icon={mdiFastForward} {size} />
|
||||
<span class="uppercase">{$t('resume')}</span>
|
||||
</JobTileButton>
|
||||
{:else}
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Pause, force: false })}>
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}>
|
||||
<Icon icon={mdiPause} size="24" />
|
||||
<span class="uppercase">{$t('pause')}</span>
|
||||
</JobTileButton>
|
||||
@ -162,25 +162,25 @@
|
||||
|
||||
{#if !disabled && multipleButtons && isIdle}
|
||||
{#if allText}
|
||||
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: JobCommand.Start, force: true })}>
|
||||
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}>
|
||||
<Icon icon={mdiAllInclusive} size="24" />
|
||||
<span class="uppercase">{allText}</span>
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
{#if refreshText}
|
||||
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Start, force: undefined })}>
|
||||
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}>
|
||||
<Icon icon={mdiImageRefreshOutline} size="24" />
|
||||
<span class="uppercase">{refreshText}</span>
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
|
||||
<Icon icon={mdiSelectionSearch} size="24" />
|
||||
<span class="uppercase">{missingText}</span>
|
||||
</JobTileButton>
|
||||
{/if}
|
||||
|
||||
{#if !disabled && !multipleButtons && isIdle}
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
|
||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
|
||||
<Icon icon={mdiPlay} size="48" />
|
||||
<span class="uppercase">{missingText}</span>
|
||||
</JobTileButton>
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { getJobName } from '$lib/utils';
|
||||
import { getQueueName } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { JobCommand, JobName, sendJobCommand, type AllJobStatusResponseDto, type JobCommandDto } from '@immich/sdk';
|
||||
import {
|
||||
QueueCommand,
|
||||
type QueueCommandDto,
|
||||
QueueName,
|
||||
type QueuesResponseDto,
|
||||
runQueueCommandLegacy,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager } from '@immich/ui';
|
||||
import {
|
||||
mdiContentDuplicate,
|
||||
@ -23,7 +29,7 @@
|
||||
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
|
||||
|
||||
interface Props {
|
||||
jobs: AllJobStatusResponseDto;
|
||||
jobs: QueuesResponseDto;
|
||||
}
|
||||
|
||||
let { jobs = $bindable() }: Props = $props();
|
||||
@ -38,17 +44,17 @@
|
||||
missingText: string;
|
||||
disabled?: boolean;
|
||||
icon: string;
|
||||
handleCommand?: (jobId: JobName, jobCommand: JobCommandDto) => Promise<void>;
|
||||
handleCommand?: (jobId: QueueName, jobCommand: QueueCommandDto) => Promise<void>;
|
||||
};
|
||||
|
||||
const handleConfirmCommand = async (jobId: JobName, dto: JobCommandDto) => {
|
||||
const handleConfirmCommand = async (jobId: QueueName, dto: QueueCommandDto) => {
|
||||
if (dto.force) {
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('admin.confirm_reprocess_all_faces'),
|
||||
});
|
||||
|
||||
if (isConfirmed) {
|
||||
await handleCommand(jobId, { command: JobCommand.Start, force: true });
|
||||
await handleCommand(jobId, { command: QueueCommand.Start, force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -58,54 +64,54 @@
|
||||
await handleCommand(jobId, dto);
|
||||
};
|
||||
|
||||
let jobDetails: Partial<Record<JobName, JobDetails>> = {
|
||||
[JobName.ThumbnailGeneration]: {
|
||||
let jobDetails: Partial<Record<QueueName, JobDetails>> = {
|
||||
[QueueName.ThumbnailGeneration]: {
|
||||
icon: mdiFileJpgBox,
|
||||
title: $getJobName(JobName.ThumbnailGeneration),
|
||||
title: $getQueueName(QueueName.ThumbnailGeneration),
|
||||
subtitle: $t('admin.thumbnail_generation_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
},
|
||||
[JobName.MetadataExtraction]: {
|
||||
[QueueName.MetadataExtraction]: {
|
||||
icon: mdiTable,
|
||||
title: $getJobName(JobName.MetadataExtraction),
|
||||
title: $getQueueName(QueueName.MetadataExtraction),
|
||||
subtitle: $t('admin.metadata_extraction_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
},
|
||||
[JobName.Library]: {
|
||||
[QueueName.Library]: {
|
||||
icon: mdiLibraryShelves,
|
||||
title: $getJobName(JobName.Library),
|
||||
title: $getQueueName(QueueName.Library),
|
||||
subtitle: $t('admin.library_tasks_description'),
|
||||
missingText: $t('rescan'),
|
||||
},
|
||||
[JobName.Sidecar]: {
|
||||
title: $getJobName(JobName.Sidecar),
|
||||
[QueueName.Sidecar]: {
|
||||
title: $getQueueName(QueueName.Sidecar),
|
||||
icon: mdiFileXmlBox,
|
||||
subtitle: $t('admin.sidecar_job_description'),
|
||||
allText: $t('sync'),
|
||||
missingText: $t('discover'),
|
||||
disabled: !featureFlags.sidecar,
|
||||
},
|
||||
[JobName.SmartSearch]: {
|
||||
[QueueName.SmartSearch]: {
|
||||
icon: mdiImageSearch,
|
||||
title: $getJobName(JobName.SmartSearch),
|
||||
title: $getQueueName(QueueName.SmartSearch),
|
||||
subtitle: $t('admin.smart_search_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
disabled: !featureFlags.smartSearch,
|
||||
},
|
||||
[JobName.DuplicateDetection]: {
|
||||
[QueueName.DuplicateDetection]: {
|
||||
icon: mdiContentDuplicate,
|
||||
title: $getJobName(JobName.DuplicateDetection),
|
||||
title: $getQueueName(QueueName.DuplicateDetection),
|
||||
subtitle: $t('admin.duplicate_detection_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
disabled: !featureFlags.duplicateDetection,
|
||||
},
|
||||
[JobName.FaceDetection]: {
|
||||
[QueueName.FaceDetection]: {
|
||||
icon: mdiFaceRecognition,
|
||||
title: $getJobName(JobName.FaceDetection),
|
||||
title: $getQueueName(QueueName.FaceDetection),
|
||||
subtitle: $t('admin.face_detection_description'),
|
||||
allText: $t('reset'),
|
||||
refreshText: $t('refresh'),
|
||||
@ -113,67 +119,67 @@
|
||||
handleCommand: handleConfirmCommand,
|
||||
disabled: !featureFlags.facialRecognition,
|
||||
},
|
||||
[JobName.FacialRecognition]: {
|
||||
[QueueName.FacialRecognition]: {
|
||||
icon: mdiTagFaces,
|
||||
title: $getJobName(JobName.FacialRecognition),
|
||||
title: $getQueueName(QueueName.FacialRecognition),
|
||||
subtitle: $t('admin.facial_recognition_job_description'),
|
||||
allText: $t('reset'),
|
||||
missingText: $t('missing'),
|
||||
handleCommand: handleConfirmCommand,
|
||||
disabled: !featureFlags.facialRecognition,
|
||||
},
|
||||
[JobName.Ocr]: {
|
||||
[QueueName.Ocr]: {
|
||||
icon: mdiOcr,
|
||||
title: $getJobName(JobName.Ocr),
|
||||
title: $getQueueName(QueueName.Ocr),
|
||||
subtitle: $t('admin.ocr_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
disabled: !featureFlags.ocr,
|
||||
},
|
||||
[JobName.VideoConversion]: {
|
||||
[QueueName.VideoConversion]: {
|
||||
icon: mdiVideo,
|
||||
title: $getJobName(JobName.VideoConversion),
|
||||
title: $getQueueName(QueueName.VideoConversion),
|
||||
subtitle: $t('admin.video_conversion_job_description'),
|
||||
allText: $t('all'),
|
||||
missingText: $t('missing'),
|
||||
},
|
||||
[JobName.StorageTemplateMigration]: {
|
||||
[QueueName.StorageTemplateMigration]: {
|
||||
icon: mdiFolderMove,
|
||||
title: $getJobName(JobName.StorageTemplateMigration),
|
||||
title: $getQueueName(QueueName.StorageTemplateMigration),
|
||||
missingText: $t('start'),
|
||||
description: StorageMigrationDescription,
|
||||
},
|
||||
[JobName.Migration]: {
|
||||
[QueueName.Migration]: {
|
||||
icon: mdiFolderMove,
|
||||
title: $getJobName(JobName.Migration),
|
||||
title: $getQueueName(QueueName.Migration),
|
||||
subtitle: $t('admin.migration_job_description'),
|
||||
missingText: $t('start'),
|
||||
},
|
||||
};
|
||||
|
||||
let jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
|
||||
let jobList = Object.entries(jobDetails) as [QueueName, JobDetails][];
|
||||
|
||||
async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) {
|
||||
const title = jobDetails[jobId]?.title;
|
||||
async function handleCommand(name: QueueName, dto: QueueCommandDto) {
|
||||
const title = jobDetails[name]?.title;
|
||||
|
||||
try {
|
||||
jobs[jobId] = await sendJobCommand({ id: jobId, jobCommandDto: jobCommand });
|
||||
jobs[name] = await runQueueCommandLegacy({ name, queueCommandDto: dto });
|
||||
|
||||
switch (jobCommand.command) {
|
||||
case JobCommand.Empty: {
|
||||
switch (dto.command) {
|
||||
case QueueCommand.Empty: {
|
||||
toastManager.success($t('admin.cleared_jobs', { values: { job: title } }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.failed_job_command', { values: { command: jobCommand.command, job: title } }));
|
||||
handleError(error, $t('admin.failed_job_command', { values: { command: dto.command, job: title } }));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-7">
|
||||
{#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }] (jobName)}
|
||||
{@const { jobCounts, queueStatus } = jobs[jobName]}
|
||||
{@const { jobCounts: statistics, queueStatus } = jobs[jobName]}
|
||||
<JobTile
|
||||
{icon}
|
||||
{title}
|
||||
@ -183,7 +189,7 @@
|
||||
{allText}
|
||||
{refreshText}
|
||||
{missingText}
|
||||
{jobCounts}
|
||||
{statistics}
|
||||
{queueStatus}
|
||||
onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}
|
||||
/>
|
||||
|
||||
@ -5,8 +5,8 @@ import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetMediaSize,
|
||||
JobName,
|
||||
MemoryType,
|
||||
QueueName,
|
||||
finishOAuth,
|
||||
getAssetOriginalPath,
|
||||
getAssetPlaybackPath,
|
||||
@ -143,28 +143,28 @@ export const downloadRequest = <TBody = unknown>(options: DownloadRequestOptions
|
||||
});
|
||||
};
|
||||
|
||||
export const getJobName = derived(t, ($t) => {
|
||||
return (jobName: JobName) => {
|
||||
const names: Record<JobName, string> = {
|
||||
[JobName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'),
|
||||
[JobName.MetadataExtraction]: $t('admin.metadata_extraction_job'),
|
||||
[JobName.Sidecar]: $t('admin.sidecar_job'),
|
||||
[JobName.SmartSearch]: $t('admin.machine_learning_smart_search'),
|
||||
[JobName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'),
|
||||
[JobName.FaceDetection]: $t('admin.face_detection'),
|
||||
[JobName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'),
|
||||
[JobName.VideoConversion]: $t('admin.video_conversion_job'),
|
||||
[JobName.StorageTemplateMigration]: $t('admin.storage_template_migration'),
|
||||
[JobName.Migration]: $t('admin.migration_job'),
|
||||
[JobName.BackgroundTask]: $t('admin.background_task_job'),
|
||||
[JobName.Search]: $t('search'),
|
||||
[JobName.Library]: $t('external_libraries'),
|
||||
[JobName.Notifications]: $t('notifications'),
|
||||
[JobName.BackupDatabase]: $t('admin.backup_database'),
|
||||
[JobName.Ocr]: $t('admin.machine_learning_ocr'),
|
||||
export const getQueueName = derived(t, ($t) => {
|
||||
return (name: QueueName) => {
|
||||
const names: Record<QueueName, string> = {
|
||||
[QueueName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'),
|
||||
[QueueName.MetadataExtraction]: $t('admin.metadata_extraction_job'),
|
||||
[QueueName.Sidecar]: $t('admin.sidecar_job'),
|
||||
[QueueName.SmartSearch]: $t('admin.machine_learning_smart_search'),
|
||||
[QueueName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'),
|
||||
[QueueName.FaceDetection]: $t('admin.face_detection'),
|
||||
[QueueName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'),
|
||||
[QueueName.VideoConversion]: $t('admin.video_conversion_job'),
|
||||
[QueueName.StorageTemplateMigration]: $t('admin.storage_template_migration'),
|
||||
[QueueName.Migration]: $t('admin.migration_job'),
|
||||
[QueueName.BackgroundTask]: $t('admin.background_task_job'),
|
||||
[QueueName.Search]: $t('search'),
|
||||
[QueueName.Library]: $t('external_libraries'),
|
||||
[QueueName.Notifications]: $t('notifications'),
|
||||
[QueueName.BackupDatabase]: $t('admin.backup_database'),
|
||||
[QueueName.Ocr]: $t('admin.machine_learning_ocr'),
|
||||
};
|
||||
|
||||
return names[jobName];
|
||||
return names[name];
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -5,13 +5,7 @@
|
||||
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
|
||||
import { asyncTimeout } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
getAllJobsStatus,
|
||||
JobCommand,
|
||||
sendJobCommand,
|
||||
type AllJobStatusResponseDto,
|
||||
type JobName,
|
||||
} from '@immich/sdk';
|
||||
import { getQueuesLegacy, QueueCommand, QueueName, runQueueCommandLegacy, type QueuesResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@ -24,23 +18,23 @@
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
let jobs: AllJobStatusResponseDto | undefined = $state();
|
||||
let jobs: QueuesResponseDto | undefined = $state();
|
||||
|
||||
let running = true;
|
||||
|
||||
const pausedJobs = $derived(
|
||||
Object.entries(jobs ?? {})
|
||||
.filter(([_, jobStatus]) => jobStatus.queueStatus?.isPaused)
|
||||
.map(([jobName]) => jobName as JobName),
|
||||
.filter(([_, queue]) => queue.queueStatus?.isPaused)
|
||||
.map(([name]) => name as QueueName),
|
||||
);
|
||||
|
||||
const handleResumePausedJobs = async () => {
|
||||
try {
|
||||
for (const jobName of pausedJobs) {
|
||||
await sendJobCommand({ id: jobName, jobCommandDto: { command: JobCommand.Resume, force: false } });
|
||||
for (const name of pausedJobs) {
|
||||
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
|
||||
}
|
||||
// Refresh jobs status immediately after resuming
|
||||
jobs = await getAllJobsStatus();
|
||||
jobs = await getQueuesLegacy();
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
|
||||
}
|
||||
@ -48,7 +42,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
while (running) {
|
||||
jobs = await getAllJobsStatus();
|
||||
jobs = await getQueuesLegacy();
|
||||
await asyncTimeout(5000);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getAllJobsStatus } from '@immich/sdk';
|
||||
import { getQueuesLegacy } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
|
||||
const jobs = await getAllJobsStatus();
|
||||
const jobs = await getQueuesLegacy();
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
|
||||
@ -17,10 +17,10 @@
|
||||
getAllLibraries,
|
||||
getLibraryStatistics,
|
||||
getUserAdmin,
|
||||
JobCommand,
|
||||
JobName,
|
||||
QueueCommand,
|
||||
QueueName,
|
||||
runQueueCommandLegacy,
|
||||
scanLibrary,
|
||||
sendJobCommand,
|
||||
updateLibrary,
|
||||
type LibraryResponseDto,
|
||||
type LibraryStatsResponseDto,
|
||||
@ -151,7 +151,7 @@
|
||||
|
||||
const handleScanAll = async () => {
|
||||
try {
|
||||
await sendJobCommand({ id: JobName.Library, jobCommandDto: { command: JobCommand.Start } });
|
||||
await runQueueCommandLegacy({ name: QueueName.Library, queueCommandDto: { command: QueueCommand.Start } });
|
||||
|
||||
toastManager.info($t('admin.refreshing_all_libraries'));
|
||||
} catch (error) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user