From 104fa09f69b6b8c8265953673485cc658beba6d7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 25 Nov 2025 08:19:40 -0500 Subject: [PATCH] feat: queues (#24142) --- e2e/src/utils.ts | 6 +- mobile/openapi/README.md | 17 +- mobile/openapi/lib/api.dart | 11 +- mobile/openapi/lib/api/deprecated_api.dart | 109 ++++ mobile/openapi/lib/api/jobs_api.dart | 8 +- mobile/openapi/lib/api/queues_api.dart | 308 +++++++++++ mobile/openapi/lib/api_client.dart | 20 +- mobile/openapi/lib/api_helper.dart | 6 + mobile/openapi/lib/model/job_name.dart | 244 ++++++++ mobile/openapi/lib/model/permission.dart | 18 + .../openapi/lib/model/queue_delete_dto.dart | 109 ++++ .../lib/model/queue_job_response_dto.dart | 132 +++++ .../openapi/lib/model/queue_job_status.dart | 97 ++++ .../openapi/lib/model/queue_response_dto.dart | 38 +- .../lib/model/queue_response_legacy_dto.dart | 107 ++++ ..._dto.dart => queue_status_legacy_dto.dart} | 38 +- .../openapi/lib/model/queue_update_dto.dart | 108 ++++ ...o.dart => queues_response_legacy_dto.dart} | 106 ++-- open-api/immich-openapi-specs.json | 519 +++++++++++++++++- open-api/typescript-sdk/src/fetch-client.ts | 204 ++++++- server/src/constants.ts | 2 + server/src/controllers/index.ts | 2 + server/src/controllers/job.controller.ts | 21 +- server/src/controllers/queue.controller.ts | 85 +++ server/src/dtos/queue-legacy.dto.ts | 89 +++ server/src/dtos/queue.dto.ts | 109 ++-- server/src/enum.ts | 22 + server/src/repositories/config.repository.ts | 2 +- server/src/repositories/job.repository.ts | 41 +- server/src/services/queue.service.spec.ts | 132 +++-- server/src/services/queue.service.ts | 74 ++- server/src/types.ts | 5 - .../test/repositories/job.repository.mock.ts | 4 +- server/test/small.factory.ts | 12 + web/src/lib/components/jobs/JobTile.svelte | 4 +- web/src/lib/components/jobs/JobsPanel.svelte | 4 +- web/src/routes/admin/jobs-status/+page.svelte | 10 +- 37 files changed, 2487 insertions(+), 336 deletions(-) create mode 100644 mobile/openapi/lib/api/queues_api.dart create mode 100644 mobile/openapi/lib/model/job_name.dart create mode 100644 mobile/openapi/lib/model/queue_delete_dto.dart create mode 100644 mobile/openapi/lib/model/queue_job_response_dto.dart create mode 100644 mobile/openapi/lib/model/queue_job_status.dart create mode 100644 mobile/openapi/lib/model/queue_response_legacy_dto.dart rename mobile/openapi/lib/model/{queue_status_dto.dart => queue_status_legacy_dto.dart} (63%) create mode 100644 mobile/openapi/lib/model/queue_update_dto.dart rename mobile/openapi/lib/model/{queues_response_dto.dart => queues_response_legacy_dto.dart} (55%) create mode 100644 server/src/controllers/queue.controller.ts create mode 100644 server/src/dtos/queue-legacy.dto.ts diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index f045ea2efd..15bb112cd8 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -12,7 +12,7 @@ import { PersonCreateDto, QueueCommandDto, QueueName, - QueuesResponseDto, + QueuesResponseLegacyDto, SharedLinkCreateDto, UpdateLibraryDto, UserAdminCreateDto, @@ -564,13 +564,13 @@ export const utils = { await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); }, - isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseDto) => { + isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseLegacyDto) => { const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) }); const jobCounts = queues[queue].jobCounts; return !jobCounts.active && !jobCounts.waiting; }, - waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseDto, ms?: number) => { + waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms?: number) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 52fbbc8e7b..268c4849c5 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -137,8 +137,10 @@ Class | Method | HTTP request | Description *DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user *DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user +*DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets *DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset +*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate @@ -198,6 +200,11 @@ Class | Method | HTTP request | Description *PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person *PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin *PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins +*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue +*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue +*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs +*QueuesApi* | [**getQueues**](doc//QueuesApi.md#getqueues) | **GET** /queues | List all queues +*QueuesApi* | [**updateQueue**](doc//QueuesApi.md#updatequeue) | **PUT** /queues/{name} | Update a queue *SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | Retrieve assets by city *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data *SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | Retrieve search suggestions @@ -396,6 +403,7 @@ Class | Method | HTTP request | Description - [FoldersUpdate](doc//FoldersUpdate.md) - [ImageFormat](doc//ImageFormat.md) - [JobCreateDto](doc//JobCreateDto.md) + - [JobName](doc//JobName.md) - [JobSettingsDto](doc//JobSettingsDto.md) - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) @@ -465,11 +473,16 @@ Class | Method | HTTP request | Description - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueCommand](doc//QueueCommand.md) - [QueueCommandDto](doc//QueueCommandDto.md) + - [QueueDeleteDto](doc//QueueDeleteDto.md) + - [QueueJobResponseDto](doc//QueueJobResponseDto.md) + - [QueueJobStatus](doc//QueueJobStatus.md) - [QueueName](doc//QueueName.md) - [QueueResponseDto](doc//QueueResponseDto.md) + - [QueueResponseLegacyDto](doc//QueueResponseLegacyDto.md) - [QueueStatisticsDto](doc//QueueStatisticsDto.md) - - [QueueStatusDto](doc//QueueStatusDto.md) - - [QueuesResponseDto](doc//QueuesResponseDto.md) + - [QueueStatusLegacyDto](doc//QueueStatusLegacyDto.md) + - [QueueUpdateDto](doc//QueueUpdateDto.md) + - [QueuesResponseLegacyDto](doc//QueuesResponseLegacyDto.md) - [RandomSearchDto](doc//RandomSearchDto.md) - [RatingsResponse](doc//RatingsResponse.md) - [RatingsUpdate](doc//RatingsUpdate.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a47d9ddf92..21730074aa 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -50,6 +50,7 @@ part 'api/notifications_admin_api.dart'; part 'api/partners_api.dart'; part 'api/people_api.dart'; part 'api/plugins_api.dart'; +part 'api/queues_api.dart'; part 'api/search_api.dart'; part 'api/server_api.dart'; part 'api/sessions_api.dart'; @@ -154,6 +155,7 @@ part 'model/folders_response.dart'; part 'model/folders_update.dart'; part 'model/image_format.dart'; part 'model/job_create_dto.dart'; +part 'model/job_name.dart'; part 'model/job_settings_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; @@ -223,11 +225,16 @@ part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; part 'model/queue_command.dart'; part 'model/queue_command_dto.dart'; +part 'model/queue_delete_dto.dart'; +part 'model/queue_job_response_dto.dart'; +part 'model/queue_job_status.dart'; part 'model/queue_name.dart'; part 'model/queue_response_dto.dart'; +part 'model/queue_response_legacy_dto.dart'; part 'model/queue_statistics_dto.dart'; -part 'model/queue_status_dto.dart'; -part 'model/queues_response_dto.dart'; +part 'model/queue_status_legacy_dto.dart'; +part 'model/queue_update_dto.dart'; +part 'model/queues_response_legacy_dto.dart'; part 'model/random_search_dto.dart'; part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index aaf7c074b9..d0d92d804d 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -248,6 +248,54 @@ class DeprecatedApi { return null; } + /// Retrieve queue counts and status + /// + /// Retrieve the counts of the current queue, as well as the current status. + /// + /// Note: This method returns the HTTP [Response]. + Future getQueuesLegacyWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/jobs'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve queue counts and status + /// + /// Retrieve the counts of the current queue, as well as the current status. + Future getQueuesLegacy() async { + final response = await getQueuesLegacyWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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), 'QueuesResponseLegacyDto',) as QueuesResponseLegacyDto; + + } + return null; + } + /// Get random assets /// /// Retrieve a specified number of random assets for the authenticated user. @@ -444,4 +492,65 @@ class DeprecatedApi { } return null; } + + /// Run jobs + /// + /// 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. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueCommandDto] queueCommandDto (required): + Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/jobs/{name}' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody = queueCommandDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Run jobs + /// + /// 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. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueCommandDto] queueCommandDto (required): + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { + final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 906dce6d37..9dda59a883 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -97,7 +97,7 @@ class JobsApi { /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. - Future getQueuesLegacy() async { + Future 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), 'QueuesResponseDto',) as QueuesResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseLegacyDto',) as QueuesResponseLegacyDto; } return null; @@ -158,7 +158,7 @@ class JobsApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { + Future 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), 'QueueResponseDto',) as QueueResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto; } return null; diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart new file mode 100644 index 0000000000..50575ed706 --- /dev/null +++ b/mobile/openapi/lib/api/queues_api.dart @@ -0,0 +1,308 @@ +// +// 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 QueuesApi { + QueuesApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Empty a queue + /// + /// Removes all jobs from the specified queue. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueDeleteDto] queueDeleteDto (required): + Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}/jobs' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody = queueDeleteDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Empty a queue + /// + /// Removes all jobs from the specified queue. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueDeleteDto] queueDeleteDto (required): + Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { + final response = await emptyQueueWithHttpInfo(name, queueDeleteDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Retrieve a queue + /// + /// Retrieves a specific queue by its name. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + Future getQueueWithHttpInfo(QueueName name,) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve a queue + /// + /// Retrieves a specific queue by its name. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + Future getQueue(QueueName name,) async { + final response = await getQueueWithHttpInfo(name,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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), 'QueueResponseDto',) as QueueResponseDto; + + } + return null; + } + + /// Retrieve queue jobs + /// + /// Retrieves a list of queue jobs from the specified queue. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [List] status: + Future getQueueJobsWithHttpInfo(QueueName name, { List? status, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}/jobs' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (status != null) { + queryParams.addAll(_queryParams('multi', 'status', status)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve queue jobs + /// + /// Retrieves a list of queue jobs from the specified queue. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [List] status: + Future?> getQueueJobs(QueueName name, { List? status, }) async { + final response = await getQueueJobsWithHttpInfo(name, status: status, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// List all queues + /// + /// Retrieves a list of queues. + /// + /// Note: This method returns the HTTP [Response]. + Future getQueuesWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/queues'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// List all queues + /// + /// Retrieves a list of queues. + Future?> getQueues() async { + final response = await getQueuesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Update a queue + /// + /// Change the paused status of a specific queue. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueUpdateDto] queueUpdateDto (required): + Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody = queueUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Update a queue + /// + /// Change the paused status of a specific queue. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueUpdateDto] queueUpdateDto (required): + Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { + final response = await updateQueueWithHttpInfo(name, queueUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // 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), 'QueueResponseDto',) as QueueResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index c0dcf542ef..041be67015 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -358,6 +358,8 @@ class ApiClient { return ImageFormatTypeTransformer().decode(value); case 'JobCreateDto': return JobCreateDto.fromJson(value); + case 'JobName': + return JobNameTypeTransformer().decode(value); case 'JobSettingsDto': return JobSettingsDto.fromJson(value); case 'LibraryResponseDto': @@ -496,16 +498,26 @@ class ApiClient { return QueueCommandTypeTransformer().decode(value); case 'QueueCommandDto': return QueueCommandDto.fromJson(value); + case 'QueueDeleteDto': + return QueueDeleteDto.fromJson(value); + case 'QueueJobResponseDto': + return QueueJobResponseDto.fromJson(value); + case 'QueueJobStatus': + return QueueJobStatusTypeTransformer().decode(value); case 'QueueName': return QueueNameTypeTransformer().decode(value); case 'QueueResponseDto': return QueueResponseDto.fromJson(value); + case 'QueueResponseLegacyDto': + return QueueResponseLegacyDto.fromJson(value); case 'QueueStatisticsDto': return QueueStatisticsDto.fromJson(value); - case 'QueueStatusDto': - return QueueStatusDto.fromJson(value); - case 'QueuesResponseDto': - return QueuesResponseDto.fromJson(value); + case 'QueueStatusLegacyDto': + return QueueStatusLegacyDto.fromJson(value); + case 'QueueUpdateDto': + return QueueUpdateDto.fromJson(value); + case 'QueuesResponseLegacyDto': + return QueuesResponseLegacyDto.fromJson(value); case 'RandomSearchDto': return RandomSearchDto.fromJson(value); case 'RatingsResponse': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index e6d39d5eb7..2c97eeb314 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -94,6 +94,9 @@ String parameterToString(dynamic value) { if (value is ImageFormat) { return ImageFormatTypeTransformer().encode(value).toString(); } + if (value is JobName) { + return JobNameTypeTransformer().encode(value).toString(); + } if (value is LogLevel) { return LogLevelTypeTransformer().encode(value).toString(); } @@ -133,6 +136,9 @@ String parameterToString(dynamic value) { if (value is QueueCommand) { return QueueCommandTypeTransformer().encode(value).toString(); } + if (value is QueueJobStatus) { + return QueueJobStatusTypeTransformer().encode(value).toString(); + } if (value is QueueName) { return QueueNameTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart new file mode 100644 index 0000000000..038a17a8e6 --- /dev/null +++ b/mobile/openapi/lib/model/job_name.dart @@ -0,0 +1,244 @@ +// +// 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 assetDelete = JobName._(r'AssetDelete'); + static const assetDeleteCheck = JobName._(r'AssetDeleteCheck'); + static const assetDetectFacesQueueAll = JobName._(r'AssetDetectFacesQueueAll'); + static const assetDetectFaces = JobName._(r'AssetDetectFaces'); + static const assetDetectDuplicatesQueueAll = JobName._(r'AssetDetectDuplicatesQueueAll'); + static const assetDetectDuplicates = JobName._(r'AssetDetectDuplicates'); + static const assetEncodeVideoQueueAll = JobName._(r'AssetEncodeVideoQueueAll'); + static const assetEncodeVideo = JobName._(r'AssetEncodeVideo'); + static const assetEmptyTrash = JobName._(r'AssetEmptyTrash'); + static const assetExtractMetadataQueueAll = JobName._(r'AssetExtractMetadataQueueAll'); + static const assetExtractMetadata = JobName._(r'AssetExtractMetadata'); + static const assetFileMigration = JobName._(r'AssetFileMigration'); + static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll'); + static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails'); + static const auditLogCleanup = JobName._(r'AuditLogCleanup'); + static const auditTableCleanup = JobName._(r'AuditTableCleanup'); + static const databaseBackup = JobName._(r'DatabaseBackup'); + static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); + static const facialRecognition = JobName._(r'FacialRecognition'); + static const fileDelete = JobName._(r'FileDelete'); + static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll'); + static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck'); + static const libraryDelete = JobName._(r'LibraryDelete'); + static const libraryRemoveAsset = JobName._(r'LibraryRemoveAsset'); + static const libraryScanAssetsQueueAll = JobName._(r'LibraryScanAssetsQueueAll'); + static const librarySyncAssets = JobName._(r'LibrarySyncAssets'); + static const librarySyncFilesQueueAll = JobName._(r'LibrarySyncFilesQueueAll'); + static const librarySyncFiles = JobName._(r'LibrarySyncFiles'); + static const libraryScanQueueAll = JobName._(r'LibraryScanQueueAll'); + static const memoryCleanup = JobName._(r'MemoryCleanup'); + static const memoryGenerate = JobName._(r'MemoryGenerate'); + static const notificationsCleanup = JobName._(r'NotificationsCleanup'); + static const notifyUserSignup = JobName._(r'NotifyUserSignup'); + static const notifyAlbumInvite = JobName._(r'NotifyAlbumInvite'); + static const notifyAlbumUpdate = JobName._(r'NotifyAlbumUpdate'); + static const userDelete = JobName._(r'UserDelete'); + static const userDeleteCheck = JobName._(r'UserDeleteCheck'); + static const userSyncUsage = JobName._(r'UserSyncUsage'); + static const personCleanup = JobName._(r'PersonCleanup'); + static const personFileMigration = JobName._(r'PersonFileMigration'); + static const personGenerateThumbnail = JobName._(r'PersonGenerateThumbnail'); + static const sessionCleanup = JobName._(r'SessionCleanup'); + static const sendMail = JobName._(r'SendMail'); + static const sidecarQueueAll = JobName._(r'SidecarQueueAll'); + static const sidecarCheck = JobName._(r'SidecarCheck'); + static const sidecarWrite = JobName._(r'SidecarWrite'); + static const smartSearchQueueAll = JobName._(r'SmartSearchQueueAll'); + static const smartSearch = JobName._(r'SmartSearch'); + static const storageTemplateMigration = JobName._(r'StorageTemplateMigration'); + static const storageTemplateMigrationSingle = JobName._(r'StorageTemplateMigrationSingle'); + static const tagCleanup = JobName._(r'TagCleanup'); + static const versionCheck = JobName._(r'VersionCheck'); + static const ocrQueueAll = JobName._(r'OcrQueueAll'); + static const ocr = JobName._(r'Ocr'); + static const workflowRun = JobName._(r'WorkflowRun'); + + /// List of all possible values in this [enum][JobName]. + static const values = [ + assetDelete, + assetDeleteCheck, + assetDetectFacesQueueAll, + assetDetectFaces, + assetDetectDuplicatesQueueAll, + assetDetectDuplicates, + assetEncodeVideoQueueAll, + assetEncodeVideo, + assetEmptyTrash, + assetExtractMetadataQueueAll, + assetExtractMetadata, + assetFileMigration, + assetGenerateThumbnailsQueueAll, + assetGenerateThumbnails, + auditLogCleanup, + auditTableCleanup, + databaseBackup, + facialRecognitionQueueAll, + facialRecognition, + fileDelete, + fileMigrationQueueAll, + libraryDeleteCheck, + libraryDelete, + libraryRemoveAsset, + libraryScanAssetsQueueAll, + librarySyncAssets, + librarySyncFilesQueueAll, + librarySyncFiles, + libraryScanQueueAll, + memoryCleanup, + memoryGenerate, + notificationsCleanup, + notifyUserSignup, + notifyAlbumInvite, + notifyAlbumUpdate, + userDelete, + userDeleteCheck, + userSyncUsage, + personCleanup, + personFileMigration, + personGenerateThumbnail, + sessionCleanup, + sendMail, + sidecarQueueAll, + sidecarCheck, + sidecarWrite, + smartSearchQueueAll, + smartSearch, + storageTemplateMigration, + storageTemplateMigrationSingle, + tagCleanup, + versionCheck, + ocrQueueAll, + ocr, + workflowRun, + ]; + + static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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'AssetDelete': return JobName.assetDelete; + case r'AssetDeleteCheck': return JobName.assetDeleteCheck; + case r'AssetDetectFacesQueueAll': return JobName.assetDetectFacesQueueAll; + case r'AssetDetectFaces': return JobName.assetDetectFaces; + case r'AssetDetectDuplicatesQueueAll': return JobName.assetDetectDuplicatesQueueAll; + case r'AssetDetectDuplicates': return JobName.assetDetectDuplicates; + case r'AssetEncodeVideoQueueAll': return JobName.assetEncodeVideoQueueAll; + case r'AssetEncodeVideo': return JobName.assetEncodeVideo; + case r'AssetEmptyTrash': return JobName.assetEmptyTrash; + case r'AssetExtractMetadataQueueAll': return JobName.assetExtractMetadataQueueAll; + case r'AssetExtractMetadata': return JobName.assetExtractMetadata; + case r'AssetFileMigration': return JobName.assetFileMigration; + case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll; + case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails; + case r'AuditLogCleanup': return JobName.auditLogCleanup; + case r'AuditTableCleanup': return JobName.auditTableCleanup; + case r'DatabaseBackup': return JobName.databaseBackup; + case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; + case r'FacialRecognition': return JobName.facialRecognition; + case r'FileDelete': return JobName.fileDelete; + case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll; + case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck; + case r'LibraryDelete': return JobName.libraryDelete; + case r'LibraryRemoveAsset': return JobName.libraryRemoveAsset; + case r'LibraryScanAssetsQueueAll': return JobName.libraryScanAssetsQueueAll; + case r'LibrarySyncAssets': return JobName.librarySyncAssets; + case r'LibrarySyncFilesQueueAll': return JobName.librarySyncFilesQueueAll; + case r'LibrarySyncFiles': return JobName.librarySyncFiles; + case r'LibraryScanQueueAll': return JobName.libraryScanQueueAll; + case r'MemoryCleanup': return JobName.memoryCleanup; + case r'MemoryGenerate': return JobName.memoryGenerate; + case r'NotificationsCleanup': return JobName.notificationsCleanup; + case r'NotifyUserSignup': return JobName.notifyUserSignup; + case r'NotifyAlbumInvite': return JobName.notifyAlbumInvite; + case r'NotifyAlbumUpdate': return JobName.notifyAlbumUpdate; + case r'UserDelete': return JobName.userDelete; + case r'UserDeleteCheck': return JobName.userDeleteCheck; + case r'UserSyncUsage': return JobName.userSyncUsage; + case r'PersonCleanup': return JobName.personCleanup; + case r'PersonFileMigration': return JobName.personFileMigration; + case r'PersonGenerateThumbnail': return JobName.personGenerateThumbnail; + case r'SessionCleanup': return JobName.sessionCleanup; + case r'SendMail': return JobName.sendMail; + case r'SidecarQueueAll': return JobName.sidecarQueueAll; + case r'SidecarCheck': return JobName.sidecarCheck; + case r'SidecarWrite': return JobName.sidecarWrite; + case r'SmartSearchQueueAll': return JobName.smartSearchQueueAll; + case r'SmartSearch': return JobName.smartSearch; + case r'StorageTemplateMigration': return JobName.storageTemplateMigration; + case r'StorageTemplateMigrationSingle': return JobName.storageTemplateMigrationSingle; + case r'TagCleanup': return JobName.tagCleanup; + case r'VersionCheck': return JobName.versionCheck; + case r'OcrQueueAll': return JobName.ocrQueueAll; + case r'Ocr': return JobName.ocr; + case r'WorkflowRun': return JobName.workflowRun; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [JobNameTypeTransformer] instance. + static JobNameTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 0a2f0d1791..3b9a3964b6 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -152,6 +152,12 @@ class Permission { static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read'); static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update'); static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete'); + static const queuePeriodRead = Permission._(r'queue.read'); + static const queuePeriodUpdate = Permission._(r'queue.update'); + static const queueJobPeriodCreate = Permission._(r'queueJob.create'); + static const queueJobPeriodRead = Permission._(r'queueJob.read'); + static const queueJobPeriodUpdate = Permission._(r'queueJob.update'); + static const queueJobPeriodDelete = Permission._(r'queueJob.delete'); static const workflowPeriodCreate = Permission._(r'workflow.create'); static const workflowPeriodRead = Permission._(r'workflow.read'); static const workflowPeriodUpdate = Permission._(r'workflow.update'); @@ -294,6 +300,12 @@ class Permission { userProfileImagePeriodRead, userProfileImagePeriodUpdate, userProfileImagePeriodDelete, + queuePeriodRead, + queuePeriodUpdate, + queueJobPeriodCreate, + queueJobPeriodRead, + queueJobPeriodUpdate, + queueJobPeriodDelete, workflowPeriodCreate, workflowPeriodRead, workflowPeriodUpdate, @@ -471,6 +483,12 @@ class PermissionTypeTransformer { case r'userProfileImage.read': return Permission.userProfileImagePeriodRead; case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate; case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete; + case r'queue.read': return Permission.queuePeriodRead; + case r'queue.update': return Permission.queuePeriodUpdate; + case r'queueJob.create': return Permission.queueJobPeriodCreate; + case r'queueJob.read': return Permission.queueJobPeriodRead; + case r'queueJob.update': return Permission.queueJobPeriodUpdate; + case r'queueJob.delete': return Permission.queueJobPeriodDelete; case r'workflow.create': return Permission.workflowPeriodCreate; case r'workflow.read': return Permission.workflowPeriodRead; case r'workflow.update': return Permission.workflowPeriodUpdate; diff --git a/mobile/openapi/lib/model/queue_delete_dto.dart b/mobile/openapi/lib/model/queue_delete_dto.dart new file mode 100644 index 0000000000..d319238f92 --- /dev/null +++ b/mobile/openapi/lib/model/queue_delete_dto.dart @@ -0,0 +1,109 @@ +// +// 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 QueueDeleteDto { + /// Returns a new [QueueDeleteDto] instance. + QueueDeleteDto({ + this.failed, + }); + + /// If true, will also remove failed jobs from the queue. + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? failed; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueDeleteDto && + other.failed == failed; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (failed == null ? 0 : failed!.hashCode); + + @override + String toString() => 'QueueDeleteDto[failed=$failed]'; + + Map toJson() { + final json = {}; + if (this.failed != null) { + json[r'failed'] = this.failed; + } else { + // json[r'failed'] = null; + } + return json; + } + + /// Returns a new [QueueDeleteDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "QueueDeleteDto"); + if (value is Map) { + final json = value.cast(); + + return QueueDeleteDto( + failed: mapValueOfType(json, r'failed'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueDeleteDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueDeleteDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueDeleteDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueDeleteDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart new file mode 100644 index 0000000000..1bfaa56195 --- /dev/null +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -0,0 +1,132 @@ +// +// 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 QueueJobResponseDto { + /// Returns a new [QueueJobResponseDto] instance. + QueueJobResponseDto({ + required this.data, + this.id, + required this.name, + required this.timestamp, + }); + + Object data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? id; + + JobName name; + + int timestamp; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto && + other.data == data && + other.id == id && + other.name == name && + other.timestamp == timestamp; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (data.hashCode) + + (id == null ? 0 : id!.hashCode) + + (name.hashCode) + + (timestamp.hashCode); + + @override + String toString() => 'QueueJobResponseDto[data=$data, id=$id, name=$name, timestamp=$timestamp]'; + + Map toJson() { + final json = {}; + json[r'data'] = this.data; + if (this.id != null) { + json[r'id'] = this.id; + } else { + // json[r'id'] = null; + } + json[r'name'] = this.name; + json[r'timestamp'] = this.timestamp; + return json; + } + + /// Returns a new [QueueJobResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueJobResponseDto? fromJson(dynamic value) { + upgradeDto(value, "QueueJobResponseDto"); + if (value is Map) { + final json = value.cast(); + + return QueueJobResponseDto( + data: mapValueOfType(json, r'data')!, + id: mapValueOfType(json, r'id'), + name: JobName.fromJson(json[r'name'])!, + timestamp: mapValueOfType(json, r'timestamp')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueJobResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueJobResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueJobResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueJobResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'data', + 'name', + 'timestamp', + }; +} + diff --git a/mobile/openapi/lib/model/queue_job_status.dart b/mobile/openapi/lib/model/queue_job_status.dart new file mode 100644 index 0000000000..03a1371cc5 --- /dev/null +++ b/mobile/openapi/lib/model/queue_job_status.dart @@ -0,0 +1,97 @@ +// +// 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 QueueJobStatus { + /// Instantiate a new enum with the provided [value]. + const QueueJobStatus._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const active = QueueJobStatus._(r'active'); + static const failed = QueueJobStatus._(r'failed'); + static const completed = QueueJobStatus._(r'completed'); + static const delayed = QueueJobStatus._(r'delayed'); + static const waiting = QueueJobStatus._(r'waiting'); + static const paused = QueueJobStatus._(r'paused'); + + /// List of all possible values in this [enum][QueueJobStatus]. + static const values = [ + active, + failed, + completed, + delayed, + waiting, + paused, + ]; + + static QueueJobStatus? fromJson(dynamic value) => QueueJobStatusTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueJobStatus.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [QueueJobStatus] to String, +/// and [decode] dynamic data back to [QueueJobStatus]. +class QueueJobStatusTypeTransformer { + factory QueueJobStatusTypeTransformer() => _instance ??= const QueueJobStatusTypeTransformer._(); + + const QueueJobStatusTypeTransformer._(); + + String encode(QueueJobStatus data) => data.value; + + /// Decodes a [dynamic value][data] to a QueueJobStatus. + /// + /// 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. + QueueJobStatus? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'active': return QueueJobStatus.active; + case r'failed': return QueueJobStatus.failed; + case r'completed': return QueueJobStatus.completed; + case r'delayed': return QueueJobStatus.delayed; + case r'waiting': return QueueJobStatus.waiting; + case r'paused': return QueueJobStatus.paused; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [QueueJobStatusTypeTransformer] instance. + static QueueJobStatusTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index b20449f721..c5d4ed8e3d 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -13,32 +13,38 @@ part of openapi.api; class QueueResponseDto { /// Returns a new [QueueResponseDto] instance. QueueResponseDto({ - required this.jobCounts, - required this.queueStatus, + required this.isPaused, + required this.name, + required this.statistics, }); - QueueStatisticsDto jobCounts; + bool isPaused; - QueueStatusDto queueStatus; + QueueName name; + + QueueStatisticsDto statistics; @override bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto && - other.jobCounts == jobCounts && - other.queueStatus == queueStatus; + other.isPaused == isPaused && + other.name == name && + other.statistics == statistics; @override int get hashCode => // ignore: unnecessary_parenthesis - (jobCounts.hashCode) + - (queueStatus.hashCode); + (isPaused.hashCode) + + (name.hashCode) + + (statistics.hashCode); @override - String toString() => 'QueueResponseDto[jobCounts=$jobCounts, queueStatus=$queueStatus]'; + String toString() => 'QueueResponseDto[isPaused=$isPaused, name=$name, statistics=$statistics]'; Map toJson() { final json = {}; - json[r'jobCounts'] = this.jobCounts; - json[r'queueStatus'] = this.queueStatus; + json[r'isPaused'] = this.isPaused; + json[r'name'] = this.name; + json[r'statistics'] = this.statistics; return json; } @@ -51,8 +57,9 @@ class QueueResponseDto { final json = value.cast(); return QueueResponseDto( - jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!, - queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!, + isPaused: mapValueOfType(json, r'isPaused')!, + name: QueueName.fromJson(json[r'name'])!, + statistics: QueueStatisticsDto.fromJson(json[r'statistics'])!, ); } return null; @@ -100,8 +107,9 @@ class QueueResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'jobCounts', - 'queueStatus', + 'isPaused', + 'name', + 'statistics', }; } diff --git a/mobile/openapi/lib/model/queue_response_legacy_dto.dart b/mobile/openapi/lib/model/queue_response_legacy_dto.dart new file mode 100644 index 0000000000..214b0b31f6 --- /dev/null +++ b/mobile/openapi/lib/model/queue_response_legacy_dto.dart @@ -0,0 +1,107 @@ +// +// 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 QueueResponseLegacyDto { + /// Returns a new [QueueResponseLegacyDto] instance. + QueueResponseLegacyDto({ + required this.jobCounts, + required this.queueStatus, + }); + + QueueStatisticsDto jobCounts; + + QueueStatusLegacyDto queueStatus; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueResponseLegacyDto && + other.jobCounts == jobCounts && + other.queueStatus == queueStatus; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (jobCounts.hashCode) + + (queueStatus.hashCode); + + @override + String toString() => 'QueueResponseLegacyDto[jobCounts=$jobCounts, queueStatus=$queueStatus]'; + + Map toJson() { + final json = {}; + json[r'jobCounts'] = this.jobCounts; + json[r'queueStatus'] = this.queueStatus; + return json; + } + + /// Returns a new [QueueResponseLegacyDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueResponseLegacyDto? fromJson(dynamic value) { + upgradeDto(value, "QueueResponseLegacyDto"); + if (value is Map) { + final json = value.cast(); + + return QueueResponseLegacyDto( + jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!, + queueStatus: QueueStatusLegacyDto.fromJson(json[r'queueStatus'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueResponseLegacyDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueResponseLegacyDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueResponseLegacyDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueResponseLegacyDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'jobCounts', + 'queueStatus', + }; +} + diff --git a/mobile/openapi/lib/model/queue_status_dto.dart b/mobile/openapi/lib/model/queue_status_legacy_dto.dart similarity index 63% rename from mobile/openapi/lib/model/queue_status_dto.dart rename to mobile/openapi/lib/model/queue_status_legacy_dto.dart index 77591affe2..88c4eac340 100644 --- a/mobile/openapi/lib/model/queue_status_dto.dart +++ b/mobile/openapi/lib/model/queue_status_legacy_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class QueueStatusDto { - /// Returns a new [QueueStatusDto] instance. - QueueStatusDto({ +class QueueStatusLegacyDto { + /// Returns a new [QueueStatusLegacyDto] instance. + QueueStatusLegacyDto({ required this.isActive, required this.isPaused, }); @@ -22,7 +22,7 @@ class QueueStatusDto { bool isPaused; @override - bool operator ==(Object other) => identical(this, other) || other is QueueStatusDto && + bool operator ==(Object other) => identical(this, other) || other is QueueStatusLegacyDto && other.isActive == isActive && other.isPaused == isPaused; @@ -33,7 +33,7 @@ class QueueStatusDto { (isPaused.hashCode); @override - String toString() => 'QueueStatusDto[isActive=$isActive, isPaused=$isPaused]'; + String toString() => 'QueueStatusLegacyDto[isActive=$isActive, isPaused=$isPaused]'; Map toJson() { final json = {}; @@ -42,15 +42,15 @@ class QueueStatusDto { return json; } - /// Returns a new [QueueStatusDto] instance and imports its values from + /// Returns a new [QueueStatusLegacyDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static QueueStatusDto? fromJson(dynamic value) { - upgradeDto(value, "QueueStatusDto"); + static QueueStatusLegacyDto? fromJson(dynamic value) { + upgradeDto(value, "QueueStatusLegacyDto"); if (value is Map) { final json = value.cast(); - return QueueStatusDto( + return QueueStatusLegacyDto( isActive: mapValueOfType(json, r'isActive')!, isPaused: mapValueOfType(json, r'isPaused')!, ); @@ -58,11 +58,11 @@ class QueueStatusDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = QueueStatusDto.fromJson(row); + final value = QueueStatusLegacyDto.fromJson(row); if (value != null) { result.add(value); } @@ -71,12 +71,12 @@ class QueueStatusDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = QueueStatusDto.fromJson(entry.value); + final value = QueueStatusLegacyDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -85,14 +85,14 @@ class QueueStatusDto { return map; } - // maps a json object with a list of QueueStatusDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of QueueStatusLegacyDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = QueueStatusDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = QueueStatusLegacyDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/queue_update_dto.dart b/mobile/openapi/lib/model/queue_update_dto.dart new file mode 100644 index 0000000000..ce89e51878 --- /dev/null +++ b/mobile/openapi/lib/model/queue_update_dto.dart @@ -0,0 +1,108 @@ +// +// 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 QueueUpdateDto { + /// Returns a new [QueueUpdateDto] instance. + QueueUpdateDto({ + this.isPaused, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isPaused; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueUpdateDto && + other.isPaused == isPaused; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isPaused == null ? 0 : isPaused!.hashCode); + + @override + String toString() => 'QueueUpdateDto[isPaused=$isPaused]'; + + Map toJson() { + final json = {}; + if (this.isPaused != null) { + json[r'isPaused'] = this.isPaused; + } else { + // json[r'isPaused'] = null; + } + return json; + } + + /// Returns a new [QueueUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "QueueUpdateDto"); + if (value is Map) { + final json = value.cast(); + + return QueueUpdateDto( + isPaused: mapValueOfType(json, r'isPaused'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/queues_response_dto.dart b/mobile/openapi/lib/model/queues_response_legacy_dto.dart similarity index 55% rename from mobile/openapi/lib/model/queues_response_dto.dart rename to mobile/openapi/lib/model/queues_response_legacy_dto.dart index be40a56fb1..4aab6d863b 100644 --- a/mobile/openapi/lib/model/queues_response_dto.dart +++ b/mobile/openapi/lib/model/queues_response_legacy_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class QueuesResponseDto { - /// Returns a new [QueuesResponseDto] instance. - QueuesResponseDto({ +class QueuesResponseLegacyDto { + /// Returns a new [QueuesResponseLegacyDto] instance. + QueuesResponseLegacyDto({ required this.backgroundTask, required this.backupDatabase, required this.duplicateDetection, @@ -32,42 +32,42 @@ class QueuesResponseDto { required this.workflow, }); - QueueResponseDto backgroundTask; + QueueResponseLegacyDto backgroundTask; - QueueResponseDto backupDatabase; + QueueResponseLegacyDto backupDatabase; - QueueResponseDto duplicateDetection; + QueueResponseLegacyDto duplicateDetection; - QueueResponseDto faceDetection; + QueueResponseLegacyDto faceDetection; - QueueResponseDto facialRecognition; + QueueResponseLegacyDto facialRecognition; - QueueResponseDto library_; + QueueResponseLegacyDto library_; - QueueResponseDto metadataExtraction; + QueueResponseLegacyDto metadataExtraction; - QueueResponseDto migration; + QueueResponseLegacyDto migration; - QueueResponseDto notifications; + QueueResponseLegacyDto notifications; - QueueResponseDto ocr; + QueueResponseLegacyDto ocr; - QueueResponseDto search; + QueueResponseLegacyDto search; - QueueResponseDto sidecar; + QueueResponseLegacyDto sidecar; - QueueResponseDto smartSearch; + QueueResponseLegacyDto smartSearch; - QueueResponseDto storageTemplateMigration; + QueueResponseLegacyDto storageTemplateMigration; - QueueResponseDto thumbnailGeneration; + QueueResponseLegacyDto thumbnailGeneration; - QueueResponseDto videoConversion; + QueueResponseLegacyDto videoConversion; - QueueResponseDto workflow; + QueueResponseLegacyDto workflow; @override - bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto && + bool operator ==(Object other) => identical(this, other) || other is QueuesResponseLegacyDto && other.backgroundTask == backgroundTask && other.backupDatabase == backupDatabase && other.duplicateDetection == duplicateDetection && @@ -108,7 +108,7 @@ class QueuesResponseDto { (workflow.hashCode); @override - 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, workflow=$workflow]'; + String toString() => 'QueuesResponseLegacyDto[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, workflow=$workflow]'; Map toJson() { final json = {}; @@ -132,42 +132,42 @@ class QueuesResponseDto { return json; } - /// Returns a new [QueuesResponseDto] instance and imports its values from + /// Returns a new [QueuesResponseLegacyDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static QueuesResponseDto? fromJson(dynamic value) { - upgradeDto(value, "QueuesResponseDto"); + static QueuesResponseLegacyDto? fromJson(dynamic value) { + upgradeDto(value, "QueuesResponseLegacyDto"); if (value is Map) { final json = value.cast(); - 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'])!, - workflow: QueueResponseDto.fromJson(json[r'workflow'])!, + return QueuesResponseLegacyDto( + backgroundTask: QueueResponseLegacyDto.fromJson(json[r'backgroundTask'])!, + backupDatabase: QueueResponseLegacyDto.fromJson(json[r'backupDatabase'])!, + duplicateDetection: QueueResponseLegacyDto.fromJson(json[r'duplicateDetection'])!, + faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!, + facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!, + library_: QueueResponseLegacyDto.fromJson(json[r'library'])!, + metadataExtraction: QueueResponseLegacyDto.fromJson(json[r'metadataExtraction'])!, + migration: QueueResponseLegacyDto.fromJson(json[r'migration'])!, + notifications: QueueResponseLegacyDto.fromJson(json[r'notifications'])!, + ocr: QueueResponseLegacyDto.fromJson(json[r'ocr'])!, + search: QueueResponseLegacyDto.fromJson(json[r'search'])!, + sidecar: QueueResponseLegacyDto.fromJson(json[r'sidecar'])!, + smartSearch: QueueResponseLegacyDto.fromJson(json[r'smartSearch'])!, + storageTemplateMigration: QueueResponseLegacyDto.fromJson(json[r'storageTemplateMigration'])!, + thumbnailGeneration: QueueResponseLegacyDto.fromJson(json[r'thumbnailGeneration'])!, + videoConversion: QueueResponseLegacyDto.fromJson(json[r'videoConversion'])!, + workflow: QueueResponseLegacyDto.fromJson(json[r'workflow'])!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = QueuesResponseDto.fromJson(row); + final value = QueuesResponseLegacyDto.fromJson(row); if (value != null) { result.add(value); } @@ -176,12 +176,12 @@ class QueuesResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = QueuesResponseDto.fromJson(entry.value); + final value = QueuesResponseLegacyDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -190,14 +190,14 @@ class QueuesResponseDto { return map; } - // maps a json object with a list of QueuesResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of QueuesResponseLegacyDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = QueuesResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = QueuesResponseLegacyDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index ffaad85906..68af1438cd 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4929,6 +4929,7 @@ }, "/jobs": { "get": { + "deprecated": true, "description": "Retrieve the counts of the current queue, as well as the current status.", "operationId": "getQueuesLegacy", "parameters": [], @@ -4937,7 +4938,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueuesResponseDto" + "$ref": "#/components/schemas/QueuesResponseLegacyDto" } } }, @@ -4957,7 +4958,8 @@ ], "summary": "Retrieve queue counts and status", "tags": [ - "Jobs" + "Jobs", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -4972,10 +4974,14 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v2.4.0", + "state": "Deprecated" } ], "x-immich-permission": "job.read", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" }, "post": { "description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.", @@ -5032,6 +5038,7 @@ }, "/jobs/{name}": { "put": { + "deprecated": true, "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": "runQueueCommandLegacy", "parameters": [ @@ -5059,7 +5066,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" } } }, @@ -5079,7 +5086,8 @@ ], "summary": "Run jobs", "tags": [ - "Jobs" + "Jobs", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -5094,10 +5102,14 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v2.4.0", + "state": "Deprecated" } ], "x-immich-permission": "job.create", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, "/libraries": { @@ -8064,6 +8076,303 @@ "x-immich-state": "Alpha" } }, + "/queues": { + "get": { + "description": "Retrieves a list of queues.", + "operationId": "getQueues", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/QueueResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List all queues", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.read", + "x-immich-state": "Alpha" + } + }, + "/queues/{name}": { + "get": { + "description": "Retrieves a specific queue by its name.", + "operationId": "getQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve a queue", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.read", + "x-immich-state": "Alpha" + }, + "put": { + "description": "Change the paused status of a specific queue.", + "operationId": "updateQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update a queue", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.update", + "x-immich-state": "Alpha" + } + }, + "/queues/{name}/jobs": { + "delete": { + "description": "Removes all jobs from the specified queue.", + "operationId": "emptyQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueDeleteDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Empty a queue", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queueJob.delete", + "x-immich-state": "Alpha" + }, + "get": { + "description": "Retrieves a list of queue jobs from the specified queue.", + "operationId": "getQueueJobs", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueJobStatus" + } + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/QueueJobResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve queue jobs", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queueJob.read", + "x-immich-state": "Alpha" + } + }, "/search/cities": { "get": { "description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.", @@ -14043,6 +14352,10 @@ "name": "Plugins", "description": "A plugin is an installed module that makes filters and actions available for the workflow feature." }, + { + "name": "Queues", + "description": "Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed." + }, { "name": "Search", "description": "Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting." @@ -16291,6 +16604,66 @@ ], "type": "object" }, + "JobName": { + "enum": [ + "AssetDelete", + "AssetDeleteCheck", + "AssetDetectFacesQueueAll", + "AssetDetectFaces", + "AssetDetectDuplicatesQueueAll", + "AssetDetectDuplicates", + "AssetEncodeVideoQueueAll", + "AssetEncodeVideo", + "AssetEmptyTrash", + "AssetExtractMetadataQueueAll", + "AssetExtractMetadata", + "AssetFileMigration", + "AssetGenerateThumbnailsQueueAll", + "AssetGenerateThumbnails", + "AuditLogCleanup", + "AuditTableCleanup", + "DatabaseBackup", + "FacialRecognitionQueueAll", + "FacialRecognition", + "FileDelete", + "FileMigrationQueueAll", + "LibraryDeleteCheck", + "LibraryDelete", + "LibraryRemoveAsset", + "LibraryScanAssetsQueueAll", + "LibrarySyncAssets", + "LibrarySyncFilesQueueAll", + "LibrarySyncFiles", + "LibraryScanQueueAll", + "MemoryCleanup", + "MemoryGenerate", + "NotificationsCleanup", + "NotifyUserSignup", + "NotifyAlbumInvite", + "NotifyAlbumUpdate", + "UserDelete", + "UserDeleteCheck", + "UserSyncUsage", + "PersonCleanup", + "PersonFileMigration", + "PersonGenerateThumbnail", + "SessionCleanup", + "SendMail", + "SidecarQueueAll", + "SidecarCheck", + "SidecarWrite", + "SmartSearchQueueAll", + "SmartSearch", + "StorageTemplateMigration", + "StorageTemplateMigrationSingle", + "TagCleanup", + "VersionCheck", + "OcrQueueAll", + "Ocr", + "WorkflowRun" + ], + "type": "string" + }, "JobSettingsDto": { "properties": { "concurrency": { @@ -17583,6 +17956,12 @@ "userProfileImage.read", "userProfileImage.update", "userProfileImage.delete", + "queue.read", + "queue.update", + "queueJob.create", + "queueJob.read", + "queueJob.update", + "queueJob.delete", "workflow.create", "workflow.read", "workflow.update", @@ -18083,6 +18462,63 @@ ], "type": "object" }, + "QueueDeleteDto": { + "properties": { + "failed": { + "description": "If true, will also remove failed jobs from the queue.", + "type": "boolean", + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, + "type": "object" + }, + "QueueJobResponseDto": { + "properties": { + "data": { + "type": "object" + }, + "id": { + "type": "string" + }, + "name": { + "allOf": [ + { + "$ref": "#/components/schemas/JobName" + } + ] + }, + "timestamp": { + "type": "integer" + } + }, + "required": [ + "data", + "name", + "timestamp" + ], + "type": "object" + }, + "QueueJobStatus": { + "enum": [ + "active", + "failed", + "completed", + "delayed", + "waiting", + "paused" + ], + "type": "string" + }, "QueueName": { "enum": [ "thumbnailGeneration", @@ -18106,12 +18542,35 @@ "type": "string" }, "QueueResponseDto": { + "properties": { + "isPaused": { + "type": "boolean" + }, + "name": { + "allOf": [ + { + "$ref": "#/components/schemas/QueueName" + } + ] + }, + "statistics": { + "$ref": "#/components/schemas/QueueStatisticsDto" + } + }, + "required": [ + "isPaused", + "name", + "statistics" + ], + "type": "object" + }, + "QueueResponseLegacyDto": { "properties": { "jobCounts": { "$ref": "#/components/schemas/QueueStatisticsDto" }, "queueStatus": { - "$ref": "#/components/schemas/QueueStatusDto" + "$ref": "#/components/schemas/QueueStatusLegacyDto" } }, "required": [ @@ -18151,7 +18610,7 @@ ], "type": "object" }, - "QueueStatusDto": { + "QueueStatusLegacyDto": { "properties": { "isActive": { "type": "boolean" @@ -18166,58 +18625,66 @@ ], "type": "object" }, - "QueuesResponseDto": { + "QueueUpdateDto": { + "properties": { + "isPaused": { + "type": "boolean" + } + }, + "type": "object" + }, + "QueuesResponseLegacyDto": { "properties": { "backgroundTask": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "backupDatabase": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "duplicateDetection": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "faceDetection": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "facialRecognition": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "library": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "metadataExtraction": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "migration": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "notifications": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "ocr": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "search": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "sidecar": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "smartSearch": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "storageTemplateMigration": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "thumbnailGeneration": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "videoConversion": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "workflow": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" } }, "required": [ diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0de0e7696b..bbcc2311b6 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -716,32 +716,32 @@ export type QueueStatisticsDto = { paused: number; waiting: number; }; -export type QueueStatusDto = { +export type QueueStatusLegacyDto = { isActive: boolean; isPaused: boolean; }; -export type QueueResponseDto = { +export type QueueResponseLegacyDto = { jobCounts: QueueStatisticsDto; - queueStatus: QueueStatusDto; + queueStatus: QueueStatusLegacyDto; }; -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; - workflow: QueueResponseDto; +export type QueuesResponseLegacyDto = { + backgroundTask: QueueResponseLegacyDto; + backupDatabase: QueueResponseLegacyDto; + duplicateDetection: QueueResponseLegacyDto; + faceDetection: QueueResponseLegacyDto; + facialRecognition: QueueResponseLegacyDto; + library: QueueResponseLegacyDto; + metadataExtraction: QueueResponseLegacyDto; + migration: QueueResponseLegacyDto; + notifications: QueueResponseLegacyDto; + ocr: QueueResponseLegacyDto; + search: QueueResponseLegacyDto; + sidecar: QueueResponseLegacyDto; + smartSearch: QueueResponseLegacyDto; + storageTemplateMigration: QueueResponseLegacyDto; + thumbnailGeneration: QueueResponseLegacyDto; + videoConversion: QueueResponseLegacyDto; + workflow: QueueResponseLegacyDto; }; export type JobCreateDto = { name: ManualJobName; @@ -966,6 +966,24 @@ export type PluginResponseDto = { updatedAt: string; version: string; }; +export type QueueResponseDto = { + isPaused: boolean; + name: QueueName; + statistics: QueueStatisticsDto; +}; +export type QueueUpdateDto = { + isPaused?: boolean; +}; +export type QueueDeleteDto = { + /** If true, will also remove failed jobs from the queue. */ + failed?: boolean; +}; +export type QueueJobResponseDto = { + data: object; + id?: string; + name: JobName; + timestamp: number; +}; export type SearchExploreItem = { data: AssetResponseDto; value: string; @@ -2925,7 +2943,7 @@ export function reassignFacesById({ id, faceDto }: { export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: QueuesResponseDto; + data: QueuesResponseLegacyDto; }>("/jobs", { ...opts })); @@ -2951,7 +2969,7 @@ export function runQueueCommandLegacy({ name, queueCommandDto }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: QueueResponseDto; + data: QueueResponseLegacyDto; }>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({ ...opts, method: "PUT", @@ -3651,6 +3669,75 @@ export function getPlugin({ id }: { ...opts })); } +/** + * List all queues + */ +export function getQueues(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueResponseDto[]; + }>("/queues", { + ...opts + })); +} +/** + * Retrieve a queue + */ +export function getQueue({ name }: { + name: QueueName; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueResponseDto; + }>(`/queues/${encodeURIComponent(name)}`, { + ...opts + })); +} +/** + * Update a queue + */ +export function updateQueue({ name, queueUpdateDto }: { + name: QueueName; + queueUpdateDto: QueueUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueResponseDto; + }>(`/queues/${encodeURIComponent(name)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: queueUpdateDto + }))); +} +/** + * Empty a queue + */ +export function emptyQueue({ name, queueDeleteDto }: { + name: QueueName; + queueDeleteDto: QueueDeleteDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/queues/${encodeURIComponent(name)}/jobs`, oazapfts.json({ + ...opts, + method: "DELETE", + body: queueDeleteDto + }))); +} +/** + * Retrieve queue jobs + */ +export function getQueueJobs({ name, status }: { + name: QueueName; + status?: QueueJobStatus[]; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueJobResponseDto[]; + }>(`/queues/${encodeURIComponent(name)}/jobs${QS.query(QS.explode({ + status + }))}`, { + ...opts + })); +} /** * Retrieve assets by city */ @@ -5241,6 +5328,12 @@ export enum Permission { UserProfileImageRead = "userProfileImage.read", UserProfileImageUpdate = "userProfileImage.update", UserProfileImageDelete = "userProfileImage.delete", + QueueRead = "queue.read", + QueueUpdate = "queue.update", + QueueJobCreate = "queueJob.create", + QueueJobRead = "queueJob.read", + QueueJobUpdate = "queueJob.update", + QueueJobDelete = "queueJob.delete", WorkflowCreate = "workflow.create", WorkflowRead = "workflow.read", WorkflowUpdate = "workflow.update", @@ -5330,6 +5423,71 @@ export enum PluginContext { Album = "album", Person = "person" } +export enum QueueJobStatus { + Active = "active", + Failed = "failed", + Completed = "completed", + Delayed = "delayed", + Waiting = "waiting", + Paused = "paused" +} +export enum JobName { + AssetDelete = "AssetDelete", + AssetDeleteCheck = "AssetDeleteCheck", + AssetDetectFacesQueueAll = "AssetDetectFacesQueueAll", + AssetDetectFaces = "AssetDetectFaces", + AssetDetectDuplicatesQueueAll = "AssetDetectDuplicatesQueueAll", + AssetDetectDuplicates = "AssetDetectDuplicates", + AssetEncodeVideoQueueAll = "AssetEncodeVideoQueueAll", + AssetEncodeVideo = "AssetEncodeVideo", + AssetEmptyTrash = "AssetEmptyTrash", + AssetExtractMetadataQueueAll = "AssetExtractMetadataQueueAll", + AssetExtractMetadata = "AssetExtractMetadata", + AssetFileMigration = "AssetFileMigration", + AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll", + AssetGenerateThumbnails = "AssetGenerateThumbnails", + AuditLogCleanup = "AuditLogCleanup", + AuditTableCleanup = "AuditTableCleanup", + DatabaseBackup = "DatabaseBackup", + FacialRecognitionQueueAll = "FacialRecognitionQueueAll", + FacialRecognition = "FacialRecognition", + FileDelete = "FileDelete", + FileMigrationQueueAll = "FileMigrationQueueAll", + LibraryDeleteCheck = "LibraryDeleteCheck", + LibraryDelete = "LibraryDelete", + LibraryRemoveAsset = "LibraryRemoveAsset", + LibraryScanAssetsQueueAll = "LibraryScanAssetsQueueAll", + LibrarySyncAssets = "LibrarySyncAssets", + LibrarySyncFilesQueueAll = "LibrarySyncFilesQueueAll", + LibrarySyncFiles = "LibrarySyncFiles", + LibraryScanQueueAll = "LibraryScanQueueAll", + MemoryCleanup = "MemoryCleanup", + MemoryGenerate = "MemoryGenerate", + NotificationsCleanup = "NotificationsCleanup", + NotifyUserSignup = "NotifyUserSignup", + NotifyAlbumInvite = "NotifyAlbumInvite", + NotifyAlbumUpdate = "NotifyAlbumUpdate", + UserDelete = "UserDelete", + UserDeleteCheck = "UserDeleteCheck", + UserSyncUsage = "UserSyncUsage", + PersonCleanup = "PersonCleanup", + PersonFileMigration = "PersonFileMigration", + PersonGenerateThumbnail = "PersonGenerateThumbnail", + SessionCleanup = "SessionCleanup", + SendMail = "SendMail", + SidecarQueueAll = "SidecarQueueAll", + SidecarCheck = "SidecarCheck", + SidecarWrite = "SidecarWrite", + SmartSearchQueueAll = "SmartSearchQueueAll", + SmartSearch = "SmartSearch", + StorageTemplateMigration = "StorageTemplateMigration", + StorageTemplateMigrationSingle = "StorageTemplateMigrationSingle", + TagCleanup = "TagCleanup", + VersionCheck = "VersionCheck", + OcrQueueAll = "OcrQueueAll", + Ocr = "Ocr", + WorkflowRun = "WorkflowRun" +} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/server/src/constants.ts b/server/src/constants.ts index 68534c00e9..33f8e3b4c5 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -163,6 +163,8 @@ export const endpointTags: Record = { 'A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job.', [ApiTag.Plugins]: 'A plugin is an installed module that makes filters and actions available for the workflow feature.', + [ApiTag.Queues]: + 'Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed.', [ApiTag.Search]: 'Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting.', [ApiTag.Server]: diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index d5811de48c..6ba3d38a73 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -20,6 +20,7 @@ import { OAuthController } from 'src/controllers/oauth.controller'; import { PartnerController } from 'src/controllers/partner.controller'; import { PersonController } from 'src/controllers/person.controller'; import { PluginController } from 'src/controllers/plugin.controller'; +import { QueueController } from 'src/controllers/queue.controller'; import { SearchController } from 'src/controllers/search.controller'; import { ServerController } from 'src/controllers/server.controller'; import { SessionController } from 'src/controllers/session.controller'; @@ -59,6 +60,7 @@ export const controllers = [ PartnerController, PersonController, PluginController, + QueueController, SearchController, ServerController, SessionController, diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index 977f1e0f1e..783d5a3133 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,10 +1,12 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; import { JobCreateDto } from 'src/dtos/job.dto'; -import { QueueCommandDto, QueueNameParamDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto'; +import { QueueResponseLegacyDto, QueuesResponseLegacyDto } from 'src/dtos/queue-legacy.dto'; +import { QueueCommandDto, QueueNameParamDto } from 'src/dtos/queue.dto'; import { ApiTag, Permission } from 'src/enum'; -import { Authenticated } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { JobService } from 'src/services/job.service'; import { QueueService } from 'src/services/queue.service'; @@ -21,10 +23,10 @@ export class JobController { @Endpoint({ summary: 'Retrieve queue counts and status', description: 'Retrieve the counts of the current queue, as well as the current status.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v2.4.0'), }) - getQueuesLegacy(): Promise { - return this.queueService.getAll(); + getQueuesLegacy(@Auth() auth: AuthDto): Promise { + return this.queueService.getAllLegacy(auth); } @Post() @@ -46,9 +48,12 @@ export class JobController { summary: 'Run jobs', 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.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v2.4.0'), }) - runQueueCommandLegacy(@Param() { name }: QueueNameParamDto, @Body() dto: QueueCommandDto): Promise { - return this.queueService.runCommand(name, dto); + runQueueCommandLegacy( + @Param() { name }: QueueNameParamDto, + @Body() dto: QueueCommandDto, + ): Promise { + return this.queueService.runCommandLegacy(name, dto); } } diff --git a/server/src/controllers/queue.controller.ts b/server/src/controllers/queue.controller.ts new file mode 100644 index 0000000000..1d8d918c5f --- /dev/null +++ b/server/src/controllers/queue.controller.ts @@ -0,0 +1,85 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + QueueDeleteDto, + QueueJobResponseDto, + QueueJobSearchDto, + QueueNameParamDto, + QueueResponseDto, + QueueUpdateDto, +} from 'src/dtos/queue.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { QueueService } from 'src/services/queue.service'; + +@ApiTags(ApiTag.Queues) +@Controller('queues') +export class QueueController { + constructor(private service: QueueService) {} + + @Get() + @Authenticated({ permission: Permission.QueueRead, admin: true }) + @Endpoint({ + summary: 'List all queues', + description: 'Retrieves a list of queues.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + getQueues(@Auth() auth: AuthDto): Promise { + return this.service.getAll(auth); + } + + @Get(':name') + @Authenticated({ permission: Permission.QueueRead, admin: true }) + @Endpoint({ + summary: 'Retrieve a queue', + description: 'Retrieves a specific queue by its name.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + getQueue(@Auth() auth: AuthDto, @Param() { name }: QueueNameParamDto): Promise { + return this.service.get(auth, name); + } + + @Put(':name') + @Authenticated({ permission: Permission.QueueUpdate, admin: true }) + @Endpoint({ + summary: 'Update a queue', + description: 'Change the paused status of a specific queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + updateQueue( + @Auth() auth: AuthDto, + @Param() { name }: QueueNameParamDto, + @Body() dto: QueueUpdateDto, + ): Promise { + return this.service.update(auth, name, dto); + } + + @Get(':name/jobs') + @Authenticated({ permission: Permission.QueueJobRead, admin: true }) + @Endpoint({ + summary: 'Retrieve queue jobs', + description: 'Retrieves a list of queue jobs from the specified queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + getQueueJobs( + @Auth() auth: AuthDto, + @Param() { name }: QueueNameParamDto, + @Query() dto: QueueJobSearchDto, + ): Promise { + return this.service.searchJobs(auth, name, dto); + } + + @Delete(':name/jobs') + @Authenticated({ permission: Permission.QueueJobDelete, admin: true }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Empty a queue', + description: 'Removes all jobs from the specified queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + emptyQueue(@Auth() auth: AuthDto, @Param() { name }: QueueNameParamDto, @Body() dto: QueueDeleteDto): Promise { + return this.service.emptyQueue(auth, name, dto); + } +} diff --git a/server/src/dtos/queue-legacy.dto.ts b/server/src/dtos/queue-legacy.dto.ts new file mode 100644 index 0000000000..79155e3f74 --- /dev/null +++ b/server/src/dtos/queue-legacy.dto.ts @@ -0,0 +1,89 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto'; +import { QueueName } from 'src/enum'; + +export class QueueStatusLegacyDto { + isActive!: boolean; + isPaused!: boolean; +} + +export class QueueResponseLegacyDto { + @ApiProperty({ type: QueueStatusLegacyDto }) + queueStatus!: QueueStatusLegacyDto; + + @ApiProperty({ type: QueueStatisticsDto }) + jobCounts!: QueueStatisticsDto; +} + +export class QueuesResponseLegacyDto implements Record { + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.ThumbnailGeneration]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.MetadataExtraction]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.VideoConversion]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.SmartSearch]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.StorageTemplateMigration]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Migration]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.BackgroundTask]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Search]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.DuplicateDetection]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.FaceDetection]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.FacialRecognition]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Sidecar]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Library]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Notification]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.BackupDatabase]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Ocr]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Workflow]!: QueueResponseLegacyDto; +} + +export const mapQueueLegacy = (response: QueueResponseDto): QueueResponseLegacyDto => { + return { + queueStatus: { + isPaused: response.isPaused, + isActive: response.statistics.active > 0, + }, + jobCounts: response.statistics, + }; +}; + +export const mapQueuesLegacy = (responses: QueueResponseDto[]): QueuesResponseLegacyDto => { + const legacy = new QueuesResponseLegacyDto(); + + for (const response of responses) { + legacy[response.name] = mapQueueLegacy(response); + } + + return legacy; +}; diff --git a/server/src/dtos/queue.dto.ts b/server/src/dtos/queue.dto.ts index df00c5cfc2..38a4a4ac6b 100644 --- a/server/src/dtos/queue.dto.ts +++ b/server/src/dtos/queue.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { QueueCommand, QueueName } from 'src/enum'; +import { HistoryBuilder, Property } from 'src/decorators'; +import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum'; import { ValidateBoolean, ValidateEnum } from 'src/validation'; export class QueueNameParamDto { @@ -15,6 +16,46 @@ export class QueueCommandDto { force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit } +export class QueueUpdateDto { + @ValidateBoolean({ optional: true }) + isPaused?: boolean; +} + +export class QueueDeleteDto { + @ValidateBoolean({ optional: true }) + @Property({ + description: 'If true, will also remove failed jobs from the queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + failed?: boolean; +} + +export class QueueJobSearchDto { + @ValidateEnum({ enum: QueueJobStatus, name: 'QueueJobStatus', optional: true, each: true }) + status?: QueueJobStatus[]; +} +export class QueueJobResponseDto { + id?: string; + + @ValidateEnum({ enum: JobName, name: 'JobName' }) + name!: JobName; + + data!: object; + + @ApiProperty({ type: 'integer' }) + timestamp!: number; +} + +export class QueueResponseDto { + @ValidateEnum({ enum: QueueName, name: 'QueueName' }) + name!: QueueName; + + @ValidateBoolean() + isPaused!: boolean; + + statistics!: QueueStatisticsDto; +} + export class QueueStatisticsDto { @ApiProperty({ type: 'integer' }) active!: number; @@ -29,69 +70,3 @@ export class QueueStatisticsDto { @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 { - @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; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Workflow]!: QueueResponseDto; -} diff --git a/server/src/enum.ts b/server/src/enum.ts index d397f9d2ae..87ff282f31 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -248,6 +248,14 @@ export enum Permission { UserProfileImageUpdate = 'userProfileImage.update', UserProfileImageDelete = 'userProfileImage.delete', + QueueRead = 'queue.read', + QueueUpdate = 'queue.update', + + QueueJobCreate = 'queueJob.create', + QueueJobRead = 'queueJob.read', + QueueJobUpdate = 'queueJob.update', + QueueJobDelete = 'queueJob.delete', + WorkflowCreate = 'workflow.create', WorkflowRead = 'workflow.read', WorkflowUpdate = 'workflow.update', @@ -543,6 +551,15 @@ export enum QueueName { Workflow = 'workflow', } +export enum QueueJobStatus { + Active = 'active', + Failed = 'failed', + Complete = 'completed', + Delayed = 'delayed', + Waiting = 'waiting', + Paused = 'paused', +} + export enum JobName { AssetDelete = 'AssetDelete', AssetDeleteCheck = 'AssetDeleteCheck', @@ -624,9 +641,13 @@ export enum JobName { export enum QueueCommand { Start = 'start', + /** @deprecated Use `updateQueue` instead */ Pause = 'pause', + /** @deprecated Use `updateQueue` instead */ Resume = 'resume', + /** @deprecated Use `emptyQueue` instead */ Empty = 'empty', + /** @deprecated Use `emptyQueue` instead */ ClearFailed = 'clear-failed', } @@ -823,6 +844,7 @@ export enum ApiTag { Partners = 'Partners', People = 'People', Plugins = 'Plugins', + Queues = 'Queues', Search = 'Search', Server = 'Server', Sessions = 'Sessions', diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 05d4bd2ac3..60ec021b3b 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -249,7 +249,7 @@ const getEnv = (): EnvData => { prefix: 'immich_bull', connection: { ...redisConfig }, defaultJobOptions: { - attempts: 3, + attempts: 1, removeOnComplete: true, removeOnFail: false, }, diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index cf2799a4cf..b12accb68e 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -5,11 +5,12 @@ import { JobsOptions, Queue, Worker } from 'bullmq'; import { ClassConstructor } from 'class-transformer'; import { setTimeout } from 'node:timers/promises'; import { JobConfig } from 'src/decorators'; -import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueName } from 'src/enum'; +import { QueueJobResponseDto, QueueJobSearchDto } from 'src/dtos/queue.dto'; +import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueJobStatus, QueueName } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { JobCounts, JobItem, JobOf, QueueStatus } from 'src/types'; +import { JobCounts, JobItem, JobOf } from 'src/types'; import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc'; type JobMapItem = { @@ -115,13 +116,14 @@ export class JobRepository { worker.concurrency = concurrency; } - async getQueueStatus(name: QueueName): Promise { + async isActive(name: QueueName): Promise { const queue = this.getQueue(name); + const count = await queue.getActiveCount(); + return count > 0; + } - return { - isActive: !!(await queue.getActiveCount()), - isPaused: await queue.isPaused(), - }; + async isPaused(name: QueueName): Promise { + return this.getQueue(name).isPaused(); } pause(name: QueueName) { @@ -192,17 +194,28 @@ export class JobRepository { } async waitForQueueCompletion(...queues: QueueName[]): Promise { - let activeQueue: QueueStatus | undefined; - do { - const statuses = await Promise.all(queues.map((name) => this.getQueueStatus(name))); - activeQueue = statuses.find((status) => status.isActive); - } while (activeQueue); - { - this.logger.verbose(`Waiting for ${activeQueue} queue to stop...`); + const getPending = async () => { + const results = await Promise.all(queues.map(async (name) => ({ pending: await this.isActive(name), name }))); + return results.filter(({ pending }) => pending).map(({ name }) => name); + }; + + let pending = await getPending(); + + while (pending.length > 0) { + this.logger.verbose(`Waiting for ${pending[0]} queue to stop...`); await setTimeout(1000); + pending = await getPending(); } } + async searchJobs(name: QueueName, dto: QueueJobSearchDto): Promise { + const jobs = await this.getQueue(name).getJobs(dto.status ?? Object.values(QueueJobStatus), 0, 1000); + return jobs.map((job) => { + const { id, name, timestamp, data } = job; + return { id, name: name as JobName, timestamp, data }; + }); + } + private getJobOptions(item: JobItem): JobsOptions | null { switch (item.name) { case JobName.NotifyAlbumUpdate: { diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index 5dce9476e2..f5cf20413e 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -2,6 +2,7 @@ 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 { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(QueueService.name, () => { @@ -52,80 +53,64 @@ describe(QueueService.name, () => { 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 stats = factory.queueStatistics({ active: 1 }); + const expected = { jobCounts: stats, queueStatus: { isActive: true, isPaused: true } }; - const expectedJobStatus = { - jobCounts: { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - queueStatus: { - isActive: true, - isPaused: true, - }, - }; + mocks.job.getJobCounts.mockResolvedValue(stats); + mocks.job.isPaused.mockResolvedValue(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, - [QueueName.Workflow]: expectedJobStatus, + await expect(sut.getAllLegacy(factory.auth())).resolves.toEqual({ + [QueueName.BackgroundTask]: expected, + [QueueName.DuplicateDetection]: expected, + [QueueName.SmartSearch]: expected, + [QueueName.MetadataExtraction]: expected, + [QueueName.Search]: expected, + [QueueName.StorageTemplateMigration]: expected, + [QueueName.Migration]: expected, + [QueueName.ThumbnailGeneration]: expected, + [QueueName.VideoConversion]: expected, + [QueueName.FaceDetection]: expected, + [QueueName.FacialRecognition]: expected, + [QueueName.Sidecar]: expected, + [QueueName.Library]: expected, + [QueueName.Notification]: expected, + [QueueName.BackupDatabase]: expected, + [QueueName.Ocr]: expected, + [QueueName.Workflow]: expected, }); }); }); describe('handleCommand', () => { it('should handle a pause command', async () => { - await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false }); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); + + await sut.runCommandLegacy(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 }); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); + + await sut.runCommandLegacy(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 }); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); + + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(true); await expect( - sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }), + sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalled(); @@ -133,33 +118,37 @@ describe(QueueService.name, () => { }); it('should handle a start video conversion command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.SmartSearch, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetExtractMetadataQueueAll, @@ -168,17 +157,19 @@ describe(QueueService.name, () => { }); it('should handle a start sidecar command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.Sidecar, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetGenerateThumbnailsQueueAll, @@ -187,34 +178,37 @@ describe(QueueService.name, () => { }); it('should handle a start face detection command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.FaceDetection, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(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 }); + mocks.job.isActive.mockResolvedValue(false); await expect( - sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }), + sut.runCommandLegacy(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalled(); diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index bea665e8fd..cdfa2ad2ed 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -2,7 +2,21 @@ 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 { AuthDto } from 'src/dtos/auth.dto'; +import { + mapQueueLegacy, + mapQueuesLegacy, + QueueResponseLegacyDto, + QueuesResponseLegacyDto, +} from 'src/dtos/queue-legacy.dto'; +import { + QueueCommandDto, + QueueDeleteDto, + QueueJobResponseDto, + QueueJobSearchDto, + QueueResponseDto, + QueueUpdateDto, +} from 'src/dtos/queue.dto'; import { BootstrapEventPriority, CronJob, @@ -86,7 +100,7 @@ export class QueueService extends BaseService { this.services = services; } - async runCommand(name: QueueName, dto: QueueCommandDto): Promise { + async runCommandLegacy(name: QueueName, dto: QueueCommandDto): Promise { this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`); switch (dto.command) { @@ -117,28 +131,60 @@ export class QueueService extends BaseService { } } + const response = await this.getByName(name); + + return mapQueueLegacy(response); + } + + async getAll(_auth: AuthDto): Promise { + return Promise.all(Object.values(QueueName).map((name) => this.getByName(name))); + } + + async getAllLegacy(auth: AuthDto): Promise { + const responses = await this.getAll(auth); + return mapQueuesLegacy(responses); + } + + get(auth: AuthDto, name: QueueName): Promise { return this.getByName(name); } - async getAll(): Promise { - const response = new QueuesResponseDto(); - for (const name of Object.values(QueueName)) { - response[name] = await this.getByName(name); + async update(auth: AuthDto, name: QueueName, dto: QueueUpdateDto): Promise { + if (dto.isPaused === true) { + if (name === QueueName.BackgroundTask) { + throw new BadRequestException(`The BackgroundTask queue cannot be paused`); + } + await this.jobRepository.pause(name); } - return response; + + if (dto.isPaused === false) { + await this.jobRepository.resume(name); + } + + return this.getByName(name); } - async getByName(name: QueueName): Promise { - const [jobCounts, queueStatus] = await Promise.all([ - this.jobRepository.getJobCounts(name), - this.jobRepository.getQueueStatus(name), - ]); + searchJobs(auth: AuthDto, name: QueueName, dto: QueueJobSearchDto): Promise { + return this.jobRepository.searchJobs(name, dto); + } - return { jobCounts, queueStatus }; + async emptyQueue(auth: AuthDto, name: QueueName, dto: QueueDeleteDto) { + await this.jobRepository.empty(name); + if (dto.failed) { + await this.jobRepository.clear(name, QueueCleanType.Failed); + } + } + + private async getByName(name: QueueName): Promise { + const [statistics, isPaused] = await Promise.all([ + this.jobRepository.getJobCounts(name), + this.jobRepository.isPaused(name), + ]); + return { name, isPaused, statistics }; } private async start(name: QueueName, { force }: QueueCommandDto): Promise { - const { isActive } = await this.jobRepository.getQueueStatus(name); + const isActive = await this.jobRepository.isActive(name); if (isActive) { throw new BadRequestException(`Job is already running`); } diff --git a/server/src/types.ts b/server/src/types.ts index dd3d25a7cb..848d19177d 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -291,11 +291,6 @@ export interface JobCounts { paused: number; } -export interface QueueStatus { - isActive: boolean; - isPaused: boolean; -} - export type JobItem = // Audit | { name: JobName.AuditTableCleanup; data?: IBaseJob } diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts index f0f4fdda00..4fc5460c8a 100644 --- a/server/test/repositories/job.repository.mock.ts +++ b/server/test/repositories/job.repository.mock.ts @@ -11,9 +11,11 @@ export const newJobRepositoryMock = (): Mocked Promise.resolve()), queueAll: vitest.fn().mockImplementation(() => Promise.resolve()), - getQueueStatus: vitest.fn(), + isActive: vitest.fn(), + isPaused: vitest.fn(), getJobCounts: vitest.fn(), clear: vitest.fn(), waitForQueueCompletion: vitest.fn(), diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index ea0df585ea..a0de947b2b 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -14,6 +14,7 @@ import { } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { QueueStatisticsDto } from 'src/dtos/queue.dto'; import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { OnThisDayData, UserMetadataItem } from 'src/types'; import { v4, v7 } from 'uuid'; @@ -139,6 +140,16 @@ const sessionFactory = (session: Partial = {}) => ({ ...session, }); +const queueStatisticsFactory = (dto?: Partial) => ({ + active: 0, + completed: 0, + failed: 0, + delayed: 0, + waiting: 0, + paused: 0, + ...dto, +}); + const stackFactory = () => ({ id: newUuid(), ownerId: newUuid(), @@ -353,6 +364,7 @@ export const factory = { library: libraryFactory, memory: memoryFactory, partner: partnerFactory, + queueStatistics: queueStatisticsFactory, session: sessionFactory, stack: stackFactory, user: userFactory, diff --git a/web/src/lib/components/jobs/JobTile.svelte b/web/src/lib/components/jobs/JobTile.svelte index 64a6db5b7f..8bdd7c169a 100644 --- a/web/src/lib/components/jobs/JobTile.svelte +++ b/web/src/lib/components/jobs/JobTile.svelte @@ -1,7 +1,7 @@