mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: queues (#24142)
This commit is contained in:
parent
66ae07ee39
commit
104fa09f69
37 changed files with 1110 additions and 237 deletions
|
|
@ -12,7 +12,7 @@ import {
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
QueueCommandDto,
|
QueueCommandDto,
|
||||||
QueueName,
|
QueueName,
|
||||||
QueuesResponseDto,
|
QueuesResponseLegacyDto,
|
||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
UpdateLibraryDto,
|
UpdateLibraryDto,
|
||||||
UserAdminCreateDto,
|
UserAdminCreateDto,
|
||||||
|
|
@ -564,13 +564,13 @@ export const utils = {
|
||||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
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 queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) });
|
||||||
const jobCounts = queues[queue].jobCounts;
|
const jobCounts = queues[queue].jobCounts;
|
||||||
return !jobCounts.active && !jobCounts.waiting;
|
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
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
return new Promise<void>(async (resolve, reject) => {
|
return new Promise<void>(async (resolve, reject) => {
|
||||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
|
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
|
||||||
|
|
|
||||||
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/deprecated_api.dart
generated
BIN
mobile/openapi/lib/api/deprecated_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/jobs_api.dart
generated
BIN
mobile/openapi/lib/api/jobs_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/queues_api.dart
generated
Normal file
BIN
mobile/openapi/lib/api/queues_api.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_helper.dart
generated
BIN
mobile/openapi/lib/api_helper.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/job_name.dart
generated
Normal file
BIN
mobile/openapi/lib/model/job_name.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/permission.dart
generated
BIN
mobile/openapi/lib/model/permission.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/queue_delete_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/queue_delete_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/queue_job_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/queue_job_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/queue_job_status.dart
generated
Normal file
BIN
mobile/openapi/lib/model/queue_job_status.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/queue_response_dto.dart
generated
BIN
mobile/openapi/lib/model/queue_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/queue_response_legacy_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/queue_response_legacy_dto.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/queue_update_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/queue_update_dto.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -4929,6 +4929,7 @@
|
||||||
},
|
},
|
||||||
"/jobs": {
|
"/jobs": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"deprecated": true,
|
||||||
"description": "Retrieve the counts of the current queue, as well as the current status.",
|
"description": "Retrieve the counts of the current queue, as well as the current status.",
|
||||||
"operationId": "getQueuesLegacy",
|
"operationId": "getQueuesLegacy",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
|
|
@ -4937,7 +4938,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/QueuesResponseDto"
|
"$ref": "#/components/schemas/QueuesResponseLegacyDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -4957,7 +4958,8 @@
|
||||||
],
|
],
|
||||||
"summary": "Retrieve queue counts and status",
|
"summary": "Retrieve queue counts and status",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Jobs"
|
"Jobs",
|
||||||
|
"Deprecated"
|
||||||
],
|
],
|
||||||
"x-immich-admin-only": true,
|
"x-immich-admin-only": true,
|
||||||
"x-immich-history": [
|
"x-immich-history": [
|
||||||
|
|
@ -4972,10 +4974,14 @@
|
||||||
{
|
{
|
||||||
"version": "v2",
|
"version": "v2",
|
||||||
"state": "Stable"
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.4.0",
|
||||||
|
"state": "Deprecated"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"x-immich-permission": "job.read",
|
"x-immich-permission": "job.read",
|
||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Deprecated"
|
||||||
},
|
},
|
||||||
"post": {
|
"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.",
|
"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}": {
|
"/jobs/{name}": {
|
||||||
"put": {
|
"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.",
|
"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",
|
"operationId": "runQueueCommandLegacy",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -5059,7 +5066,7 @@
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -5079,7 +5086,8 @@
|
||||||
],
|
],
|
||||||
"summary": "Run jobs",
|
"summary": "Run jobs",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Jobs"
|
"Jobs",
|
||||||
|
"Deprecated"
|
||||||
],
|
],
|
||||||
"x-immich-admin-only": true,
|
"x-immich-admin-only": true,
|
||||||
"x-immich-history": [
|
"x-immich-history": [
|
||||||
|
|
@ -5094,10 +5102,14 @@
|
||||||
{
|
{
|
||||||
"version": "v2",
|
"version": "v2",
|
||||||
"state": "Stable"
|
"state": "Stable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.4.0",
|
||||||
|
"state": "Deprecated"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"x-immich-permission": "job.create",
|
"x-immich-permission": "job.create",
|
||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Deprecated"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/libraries": {
|
"/libraries": {
|
||||||
|
|
@ -8064,6 +8076,303 @@
|
||||||
"x-immich-state": "Alpha"
|
"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": {
|
"/search/cities": {
|
||||||
"get": {
|
"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.",
|
"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",
|
"name": "Plugins",
|
||||||
"description": "A plugin is an installed module that makes filters and actions available for the workflow feature."
|
"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",
|
"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."
|
"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"
|
"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": {
|
"JobSettingsDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"concurrency": {
|
"concurrency": {
|
||||||
|
|
@ -17583,6 +17956,12 @@
|
||||||
"userProfileImage.read",
|
"userProfileImage.read",
|
||||||
"userProfileImage.update",
|
"userProfileImage.update",
|
||||||
"userProfileImage.delete",
|
"userProfileImage.delete",
|
||||||
|
"queue.read",
|
||||||
|
"queue.update",
|
||||||
|
"queueJob.create",
|
||||||
|
"queueJob.read",
|
||||||
|
"queueJob.update",
|
||||||
|
"queueJob.delete",
|
||||||
"workflow.create",
|
"workflow.create",
|
||||||
"workflow.read",
|
"workflow.read",
|
||||||
"workflow.update",
|
"workflow.update",
|
||||||
|
|
@ -18083,6 +18462,63 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"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": {
|
"QueueName": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"thumbnailGeneration",
|
"thumbnailGeneration",
|
||||||
|
|
@ -18106,12 +18542,35 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"QueueResponseDto": {
|
"QueueResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"isPaused": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/QueueName"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"statistics": {
|
||||||
|
"$ref": "#/components/schemas/QueueStatisticsDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isPaused",
|
||||||
|
"name",
|
||||||
|
"statistics"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"QueueResponseLegacyDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"jobCounts": {
|
"jobCounts": {
|
||||||
"$ref": "#/components/schemas/QueueStatisticsDto"
|
"$ref": "#/components/schemas/QueueStatisticsDto"
|
||||||
},
|
},
|
||||||
"queueStatus": {
|
"queueStatus": {
|
||||||
"$ref": "#/components/schemas/QueueStatusDto"
|
"$ref": "#/components/schemas/QueueStatusLegacyDto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -18151,7 +18610,7 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"QueueStatusDto": {
|
"QueueStatusLegacyDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"isActive": {
|
"isActive": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|
@ -18166,58 +18625,66 @@
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"QueuesResponseDto": {
|
"QueueUpdateDto": {
|
||||||
|
"properties": {
|
||||||
|
"isPaused": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"QueuesResponseLegacyDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"backgroundTask": {
|
"backgroundTask": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"backupDatabase": {
|
"backupDatabase": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"duplicateDetection": {
|
"duplicateDetection": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"faceDetection": {
|
"faceDetection": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"facialRecognition": {
|
"facialRecognition": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"metadataExtraction": {
|
"metadataExtraction": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"ocr": {
|
"ocr": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"sidecar": {
|
"sidecar": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"smartSearch": {
|
"smartSearch": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"storageTemplateMigration": {
|
"storageTemplateMigration": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"thumbnailGeneration": {
|
"thumbnailGeneration": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"videoConversion": {
|
"videoConversion": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
},
|
},
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"$ref": "#/components/schemas/QueueResponseDto"
|
"$ref": "#/components/schemas/QueueResponseLegacyDto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -716,32 +716,32 @@ export type QueueStatisticsDto = {
|
||||||
paused: number;
|
paused: number;
|
||||||
waiting: number;
|
waiting: number;
|
||||||
};
|
};
|
||||||
export type QueueStatusDto = {
|
export type QueueStatusLegacyDto = {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
};
|
};
|
||||||
export type QueueResponseDto = {
|
export type QueueResponseLegacyDto = {
|
||||||
jobCounts: QueueStatisticsDto;
|
jobCounts: QueueStatisticsDto;
|
||||||
queueStatus: QueueStatusDto;
|
queueStatus: QueueStatusLegacyDto;
|
||||||
};
|
};
|
||||||
export type QueuesResponseDto = {
|
export type QueuesResponseLegacyDto = {
|
||||||
backgroundTask: QueueResponseDto;
|
backgroundTask: QueueResponseLegacyDto;
|
||||||
backupDatabase: QueueResponseDto;
|
backupDatabase: QueueResponseLegacyDto;
|
||||||
duplicateDetection: QueueResponseDto;
|
duplicateDetection: QueueResponseLegacyDto;
|
||||||
faceDetection: QueueResponseDto;
|
faceDetection: QueueResponseLegacyDto;
|
||||||
facialRecognition: QueueResponseDto;
|
facialRecognition: QueueResponseLegacyDto;
|
||||||
library: QueueResponseDto;
|
library: QueueResponseLegacyDto;
|
||||||
metadataExtraction: QueueResponseDto;
|
metadataExtraction: QueueResponseLegacyDto;
|
||||||
migration: QueueResponseDto;
|
migration: QueueResponseLegacyDto;
|
||||||
notifications: QueueResponseDto;
|
notifications: QueueResponseLegacyDto;
|
||||||
ocr: QueueResponseDto;
|
ocr: QueueResponseLegacyDto;
|
||||||
search: QueueResponseDto;
|
search: QueueResponseLegacyDto;
|
||||||
sidecar: QueueResponseDto;
|
sidecar: QueueResponseLegacyDto;
|
||||||
smartSearch: QueueResponseDto;
|
smartSearch: QueueResponseLegacyDto;
|
||||||
storageTemplateMigration: QueueResponseDto;
|
storageTemplateMigration: QueueResponseLegacyDto;
|
||||||
thumbnailGeneration: QueueResponseDto;
|
thumbnailGeneration: QueueResponseLegacyDto;
|
||||||
videoConversion: QueueResponseDto;
|
videoConversion: QueueResponseLegacyDto;
|
||||||
workflow: QueueResponseDto;
|
workflow: QueueResponseLegacyDto;
|
||||||
};
|
};
|
||||||
export type JobCreateDto = {
|
export type JobCreateDto = {
|
||||||
name: ManualJobName;
|
name: ManualJobName;
|
||||||
|
|
@ -966,6 +966,24 @@ export type PluginResponseDto = {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
version: 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 = {
|
export type SearchExploreItem = {
|
||||||
data: AssetResponseDto;
|
data: AssetResponseDto;
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -2925,7 +2943,7 @@ export function reassignFacesById({ id, faceDto }: {
|
||||||
export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) {
|
export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: QueuesResponseDto;
|
data: QueuesResponseLegacyDto;
|
||||||
}>("/jobs", {
|
}>("/jobs", {
|
||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
|
|
@ -2951,7 +2969,7 @@ export function runQueueCommandLegacy({ name, queueCommandDto }: {
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: QueueResponseDto;
|
data: QueueResponseLegacyDto;
|
||||||
}>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({
|
}>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -3651,6 +3669,75 @@ export function getPlugin({ id }: {
|
||||||
...opts
|
...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
|
* Retrieve assets by city
|
||||||
*/
|
*/
|
||||||
|
|
@ -5241,6 +5328,12 @@ export enum Permission {
|
||||||
UserProfileImageRead = "userProfileImage.read",
|
UserProfileImageRead = "userProfileImage.read",
|
||||||
UserProfileImageUpdate = "userProfileImage.update",
|
UserProfileImageUpdate = "userProfileImage.update",
|
||||||
UserProfileImageDelete = "userProfileImage.delete",
|
UserProfileImageDelete = "userProfileImage.delete",
|
||||||
|
QueueRead = "queue.read",
|
||||||
|
QueueUpdate = "queue.update",
|
||||||
|
QueueJobCreate = "queueJob.create",
|
||||||
|
QueueJobRead = "queueJob.read",
|
||||||
|
QueueJobUpdate = "queueJob.update",
|
||||||
|
QueueJobDelete = "queueJob.delete",
|
||||||
WorkflowCreate = "workflow.create",
|
WorkflowCreate = "workflow.create",
|
||||||
WorkflowRead = "workflow.read",
|
WorkflowRead = "workflow.read",
|
||||||
WorkflowUpdate = "workflow.update",
|
WorkflowUpdate = "workflow.update",
|
||||||
|
|
@ -5330,6 +5423,71 @@ export enum PluginContext {
|
||||||
Album = "album",
|
Album = "album",
|
||||||
Person = "person"
|
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 {
|
export enum SearchSuggestionType {
|
||||||
Country = "country",
|
Country = "country",
|
||||||
State = "state",
|
State = "state",
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,8 @@ export const endpointTags: Record<ApiTag, string> = {
|
||||||
'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.',
|
'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]:
|
[ApiTag.Plugins]:
|
||||||
'A plugin is an installed module that makes filters and actions available for the workflow feature.',
|
'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]:
|
[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.',
|
'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]:
|
[ApiTag.Server]:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
import { PluginController } from 'src/controllers/plugin.controller';
|
import { PluginController } from 'src/controllers/plugin.controller';
|
||||||
|
import { QueueController } from 'src/controllers/queue.controller';
|
||||||
import { SearchController } from 'src/controllers/search.controller';
|
import { SearchController } from 'src/controllers/search.controller';
|
||||||
import { ServerController } from 'src/controllers/server.controller';
|
import { ServerController } from 'src/controllers/server.controller';
|
||||||
import { SessionController } from 'src/controllers/session.controller';
|
import { SessionController } from 'src/controllers/session.controller';
|
||||||
|
|
@ -59,6 +60,7 @@ export const controllers = [
|
||||||
PartnerController,
|
PartnerController,
|
||||||
PersonController,
|
PersonController,
|
||||||
PluginController,
|
PluginController,
|
||||||
|
QueueController,
|
||||||
SearchController,
|
SearchController,
|
||||||
ServerController,
|
ServerController,
|
||||||
SessionController,
|
SessionController,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { JobCreateDto } from 'src/dtos/job.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 { 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 { JobService } from 'src/services/job.service';
|
||||||
import { QueueService } from 'src/services/queue.service';
|
import { QueueService } from 'src/services/queue.service';
|
||||||
|
|
||||||
|
|
@ -21,10 +23,10 @@ export class JobController {
|
||||||
@Endpoint({
|
@Endpoint({
|
||||||
summary: 'Retrieve queue counts and status',
|
summary: 'Retrieve queue counts and status',
|
||||||
description: 'Retrieve the counts of the current queue, as well as the current 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<QueuesResponseDto> {
|
getQueuesLegacy(@Auth() auth: AuthDto): Promise<QueuesResponseLegacyDto> {
|
||||||
return this.queueService.getAll();
|
return this.queueService.getAllLegacy(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
|
@ -46,9 +48,12 @@ export class JobController {
|
||||||
summary: 'Run jobs',
|
summary: 'Run jobs',
|
||||||
description:
|
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.',
|
'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<QueueResponseDto> {
|
runQueueCommandLegacy(
|
||||||
return this.queueService.runCommand(name, dto);
|
@Param() { name }: QueueNameParamDto,
|
||||||
|
@Body() dto: QueueCommandDto,
|
||||||
|
): Promise<QueueResponseLegacyDto> {
|
||||||
|
return this.queueService.runCommandLegacy(name, dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
85
server/src/controllers/queue.controller.ts
Normal file
85
server/src/controllers/queue.controller.ts
Normal file
|
|
@ -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<QueueResponseDto[]> {
|
||||||
|
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<QueueResponseDto> {
|
||||||
|
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<QueueResponseDto> {
|
||||||
|
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<QueueJobResponseDto[]> {
|
||||||
|
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<void> {
|
||||||
|
return this.service.emptyQueue(auth, name, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
server/src/dtos/queue-legacy.dto.ts
Normal file
89
server/src/dtos/queue-legacy.dto.ts
Normal file
|
|
@ -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<QueueName, QueueResponseLegacyDto> {
|
||||||
|
@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;
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
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';
|
import { ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||||
|
|
||||||
export class QueueNameParamDto {
|
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
|
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 {
|
export class QueueStatisticsDto {
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
active!: number;
|
active!: number;
|
||||||
|
|
@ -29,69 +70,3 @@ export class QueueStatisticsDto {
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
paused!: number;
|
paused!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueueStatusDto {
|
|
||||||
isActive!: boolean;
|
|
||||||
isPaused!: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueueResponseDto {
|
|
||||||
@ApiProperty({ type: QueueStatisticsDto })
|
|
||||||
jobCounts!: QueueStatisticsDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueStatusDto })
|
|
||||||
queueStatus!: QueueStatusDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueuesResponseDto implements Record<QueueName, QueueResponseDto> {
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.ThumbnailGeneration]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.MetadataExtraction]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.VideoConversion]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.SmartSearch]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.StorageTemplateMigration]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Migration]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.BackgroundTask]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Search]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.DuplicateDetection]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.FaceDetection]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.FacialRecognition]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Sidecar]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Library]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Notification]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.BackupDatabase]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Ocr]!: QueueResponseDto;
|
|
||||||
|
|
||||||
@ApiProperty({ type: QueueResponseDto })
|
|
||||||
[QueueName.Workflow]!: QueueResponseDto;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,14 @@ export enum Permission {
|
||||||
UserProfileImageUpdate = 'userProfileImage.update',
|
UserProfileImageUpdate = 'userProfileImage.update',
|
||||||
UserProfileImageDelete = 'userProfileImage.delete',
|
UserProfileImageDelete = 'userProfileImage.delete',
|
||||||
|
|
||||||
|
QueueRead = 'queue.read',
|
||||||
|
QueueUpdate = 'queue.update',
|
||||||
|
|
||||||
|
QueueJobCreate = 'queueJob.create',
|
||||||
|
QueueJobRead = 'queueJob.read',
|
||||||
|
QueueJobUpdate = 'queueJob.update',
|
||||||
|
QueueJobDelete = 'queueJob.delete',
|
||||||
|
|
||||||
WorkflowCreate = 'workflow.create',
|
WorkflowCreate = 'workflow.create',
|
||||||
WorkflowRead = 'workflow.read',
|
WorkflowRead = 'workflow.read',
|
||||||
WorkflowUpdate = 'workflow.update',
|
WorkflowUpdate = 'workflow.update',
|
||||||
|
|
@ -543,6 +551,15 @@ export enum QueueName {
|
||||||
Workflow = 'workflow',
|
Workflow = 'workflow',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum QueueJobStatus {
|
||||||
|
Active = 'active',
|
||||||
|
Failed = 'failed',
|
||||||
|
Complete = 'completed',
|
||||||
|
Delayed = 'delayed',
|
||||||
|
Waiting = 'waiting',
|
||||||
|
Paused = 'paused',
|
||||||
|
}
|
||||||
|
|
||||||
export enum JobName {
|
export enum JobName {
|
||||||
AssetDelete = 'AssetDelete',
|
AssetDelete = 'AssetDelete',
|
||||||
AssetDeleteCheck = 'AssetDeleteCheck',
|
AssetDeleteCheck = 'AssetDeleteCheck',
|
||||||
|
|
@ -624,9 +641,13 @@ export enum JobName {
|
||||||
|
|
||||||
export enum QueueCommand {
|
export enum QueueCommand {
|
||||||
Start = 'start',
|
Start = 'start',
|
||||||
|
/** @deprecated Use `updateQueue` instead */
|
||||||
Pause = 'pause',
|
Pause = 'pause',
|
||||||
|
/** @deprecated Use `updateQueue` instead */
|
||||||
Resume = 'resume',
|
Resume = 'resume',
|
||||||
|
/** @deprecated Use `emptyQueue` instead */
|
||||||
Empty = 'empty',
|
Empty = 'empty',
|
||||||
|
/** @deprecated Use `emptyQueue` instead */
|
||||||
ClearFailed = 'clear-failed',
|
ClearFailed = 'clear-failed',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -823,6 +844,7 @@ export enum ApiTag {
|
||||||
Partners = 'Partners',
|
Partners = 'Partners',
|
||||||
People = 'People',
|
People = 'People',
|
||||||
Plugins = 'Plugins',
|
Plugins = 'Plugins',
|
||||||
|
Queues = 'Queues',
|
||||||
Search = 'Search',
|
Search = 'Search',
|
||||||
Server = 'Server',
|
Server = 'Server',
|
||||||
Sessions = 'Sessions',
|
Sessions = 'Sessions',
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ const getEnv = (): EnvData => {
|
||||||
prefix: 'immich_bull',
|
prefix: 'immich_bull',
|
||||||
connection: { ...redisConfig },
|
connection: { ...redisConfig },
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 1,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: false,
|
removeOnFail: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ import { JobsOptions, Queue, Worker } from 'bullmq';
|
||||||
import { ClassConstructor } from 'class-transformer';
|
import { ClassConstructor } from 'class-transformer';
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { JobConfig } from 'src/decorators';
|
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 { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.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';
|
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
|
||||||
|
|
||||||
type JobMapItem = {
|
type JobMapItem = {
|
||||||
|
|
@ -115,13 +116,14 @@ export class JobRepository {
|
||||||
worker.concurrency = concurrency;
|
worker.concurrency = concurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQueueStatus(name: QueueName): Promise<QueueStatus> {
|
async isActive(name: QueueName): Promise<boolean> {
|
||||||
const queue = this.getQueue(name);
|
const queue = this.getQueue(name);
|
||||||
|
const count = await queue.getActiveCount();
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
async isPaused(name: QueueName): Promise<boolean> {
|
||||||
isActive: !!(await queue.getActiveCount()),
|
return this.getQueue(name).isPaused();
|
||||||
isPaused: await queue.isPaused(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pause(name: QueueName) {
|
pause(name: QueueName) {
|
||||||
|
|
@ -192,17 +194,28 @@ export class JobRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForQueueCompletion(...queues: QueueName[]): Promise<void> {
|
async waitForQueueCompletion(...queues: QueueName[]): Promise<void> {
|
||||||
let activeQueue: QueueStatus | undefined;
|
const getPending = async () => {
|
||||||
do {
|
const results = await Promise.all(queues.map(async (name) => ({ pending: await this.isActive(name), name })));
|
||||||
const statuses = await Promise.all(queues.map((name) => this.getQueueStatus(name)));
|
return results.filter(({ pending }) => pending).map(({ name }) => name);
|
||||||
activeQueue = statuses.find((status) => status.isActive);
|
};
|
||||||
} while (activeQueue);
|
|
||||||
{
|
let pending = await getPending();
|
||||||
this.logger.verbose(`Waiting for ${activeQueue} queue to stop...`);
|
|
||||||
|
while (pending.length > 0) {
|
||||||
|
this.logger.verbose(`Waiting for ${pending[0]} queue to stop...`);
|
||||||
await setTimeout(1000);
|
await setTimeout(1000);
|
||||||
|
pending = await getPending();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchJobs(name: QueueName, dto: QueueJobSearchDto): Promise<QueueJobResponseDto[]> {
|
||||||
|
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 {
|
private getJobOptions(item: JobItem): JobsOptions | null {
|
||||||
switch (item.name) {
|
switch (item.name) {
|
||||||
case JobName.NotifyAlbumUpdate: {
|
case JobName.NotifyAlbumUpdate: {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common';
|
||||||
import { defaults, SystemConfig } from 'src/config';
|
import { defaults, SystemConfig } from 'src/config';
|
||||||
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
|
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
|
||||||
import { QueueService } from 'src/services/queue.service';
|
import { QueueService } from 'src/services/queue.service';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
describe(QueueService.name, () => {
|
describe(QueueService.name, () => {
|
||||||
|
|
@ -52,80 +53,64 @@ describe(QueueService.name, () => {
|
||||||
|
|
||||||
describe('getAllJobStatus', () => {
|
describe('getAllJobStatus', () => {
|
||||||
it('should get all job statuses', async () => {
|
it('should get all job statuses', async () => {
|
||||||
mocks.job.getJobCounts.mockResolvedValue({
|
const stats = factory.queueStatistics({ active: 1 });
|
||||||
active: 1,
|
const expected = { jobCounts: stats, queueStatus: { isActive: true, isPaused: true } };
|
||||||
completed: 1,
|
|
||||||
failed: 1,
|
|
||||||
delayed: 1,
|
|
||||||
waiting: 1,
|
|
||||||
paused: 1,
|
|
||||||
});
|
|
||||||
mocks.job.getQueueStatus.mockResolvedValue({
|
|
||||||
isActive: true,
|
|
||||||
isPaused: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedJobStatus = {
|
mocks.job.getJobCounts.mockResolvedValue(stats);
|
||||||
jobCounts: {
|
mocks.job.isPaused.mockResolvedValue(true);
|
||||||
active: 1,
|
|
||||||
completed: 1,
|
|
||||||
delayed: 1,
|
|
||||||
failed: 1,
|
|
||||||
waiting: 1,
|
|
||||||
paused: 1,
|
|
||||||
},
|
|
||||||
queueStatus: {
|
|
||||||
isActive: true,
|
|
||||||
isPaused: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(sut.getAll()).resolves.toEqual({
|
await expect(sut.getAllLegacy(factory.auth())).resolves.toEqual({
|
||||||
[QueueName.BackgroundTask]: expectedJobStatus,
|
[QueueName.BackgroundTask]: expected,
|
||||||
[QueueName.DuplicateDetection]: expectedJobStatus,
|
[QueueName.DuplicateDetection]: expected,
|
||||||
[QueueName.SmartSearch]: expectedJobStatus,
|
[QueueName.SmartSearch]: expected,
|
||||||
[QueueName.MetadataExtraction]: expectedJobStatus,
|
[QueueName.MetadataExtraction]: expected,
|
||||||
[QueueName.Search]: expectedJobStatus,
|
[QueueName.Search]: expected,
|
||||||
[QueueName.StorageTemplateMigration]: expectedJobStatus,
|
[QueueName.StorageTemplateMigration]: expected,
|
||||||
[QueueName.Migration]: expectedJobStatus,
|
[QueueName.Migration]: expected,
|
||||||
[QueueName.ThumbnailGeneration]: expectedJobStatus,
|
[QueueName.ThumbnailGeneration]: expected,
|
||||||
[QueueName.VideoConversion]: expectedJobStatus,
|
[QueueName.VideoConversion]: expected,
|
||||||
[QueueName.FaceDetection]: expectedJobStatus,
|
[QueueName.FaceDetection]: expected,
|
||||||
[QueueName.FacialRecognition]: expectedJobStatus,
|
[QueueName.FacialRecognition]: expected,
|
||||||
[QueueName.Sidecar]: expectedJobStatus,
|
[QueueName.Sidecar]: expected,
|
||||||
[QueueName.Library]: expectedJobStatus,
|
[QueueName.Library]: expected,
|
||||||
[QueueName.Notification]: expectedJobStatus,
|
[QueueName.Notification]: expected,
|
||||||
[QueueName.BackupDatabase]: expectedJobStatus,
|
[QueueName.BackupDatabase]: expected,
|
||||||
[QueueName.Ocr]: expectedJobStatus,
|
[QueueName.Ocr]: expected,
|
||||||
[QueueName.Workflow]: expectedJobStatus,
|
[QueueName.Workflow]: expected,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleCommand', () => {
|
describe('handleCommand', () => {
|
||||||
it('should handle a pause command', async () => {
|
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);
|
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a resume command', async () => {
|
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);
|
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an empty command', async () => {
|
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);
|
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not start a job that is already running', async () => {
|
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(
|
await expect(
|
||||||
sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
|
|
@ -133,33 +118,37 @@ describe(QueueService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start video conversion command', async () => {
|
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 } });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start storage template migration command', async () => {
|
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 });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start smart search command', async () => {
|
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 } });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start metadata extraction command', async () => {
|
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({
|
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.AssetExtractMetadataQueueAll,
|
name: JobName.AssetExtractMetadataQueueAll,
|
||||||
|
|
@ -168,17 +157,19 @@ describe(QueueService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start sidecar command', async () => {
|
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 } });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start thumbnail generation command', async () => {
|
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({
|
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.AssetGenerateThumbnailsQueueAll,
|
name: JobName.AssetGenerateThumbnailsQueueAll,
|
||||||
|
|
@ -187,34 +178,37 @@ describe(QueueService.name, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start face detection command', async () => {
|
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 } });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start facial recognition command', async () => {
|
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 } });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a start backup database command', async () => {
|
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 } });
|
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw a bad request when an invalid queue is used', async () => {
|
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(
|
await expect(
|
||||||
sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
sut.runCommandLegacy(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,21 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { ClassConstructor } from 'class-transformer';
|
import { ClassConstructor } from 'class-transformer';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { OnEvent } from 'src/decorators';
|
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 {
|
import {
|
||||||
BootstrapEventPriority,
|
BootstrapEventPriority,
|
||||||
CronJob,
|
CronJob,
|
||||||
|
|
@ -86,7 +100,7 @@ export class QueueService extends BaseService {
|
||||||
this.services = services;
|
this.services = services;
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCommand(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseDto> {
|
async runCommandLegacy(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseLegacyDto> {
|
||||||
this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`);
|
this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`);
|
||||||
|
|
||||||
switch (dto.command) {
|
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<QueueResponseDto[]> {
|
||||||
|
return Promise.all(Object.values(QueueName).map((name) => this.getByName(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllLegacy(auth: AuthDto): Promise<QueuesResponseLegacyDto> {
|
||||||
|
const responses = await this.getAll(auth);
|
||||||
|
return mapQueuesLegacy(responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(auth: AuthDto, name: QueueName): Promise<QueueResponseDto> {
|
||||||
return this.getByName(name);
|
return this.getByName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<QueuesResponseDto> {
|
async update(auth: AuthDto, name: QueueName, dto: QueueUpdateDto): Promise<QueueResponseDto> {
|
||||||
const response = new QueuesResponseDto();
|
if (dto.isPaused === true) {
|
||||||
for (const name of Object.values(QueueName)) {
|
if (name === QueueName.BackgroundTask) {
|
||||||
response[name] = await this.getByName(name);
|
throw new BadRequestException(`The BackgroundTask queue cannot be paused`);
|
||||||
}
|
}
|
||||||
return response;
|
await this.jobRepository.pause(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getByName(name: QueueName): Promise<QueueResponseDto> {
|
if (dto.isPaused === false) {
|
||||||
const [jobCounts, queueStatus] = await Promise.all([
|
await this.jobRepository.resume(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchJobs(auth: AuthDto, name: QueueName, dto: QueueJobSearchDto): Promise<QueueJobResponseDto[]> {
|
||||||
|
return this.jobRepository.searchJobs(name, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<QueueResponseDto> {
|
||||||
|
const [statistics, isPaused] = await Promise.all([
|
||||||
this.jobRepository.getJobCounts(name),
|
this.jobRepository.getJobCounts(name),
|
||||||
this.jobRepository.getQueueStatus(name),
|
this.jobRepository.isPaused(name),
|
||||||
]);
|
]);
|
||||||
|
return { name, isPaused, statistics };
|
||||||
return { jobCounts, queueStatus };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {
|
private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {
|
||||||
const { isActive } = await this.jobRepository.getQueueStatus(name);
|
const isActive = await this.jobRepository.isActive(name);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
throw new BadRequestException(`Job is already running`);
|
throw new BadRequestException(`Job is already running`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -291,11 +291,6 @@ export interface JobCounts {
|
||||||
paused: number;
|
paused: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueueStatus {
|
|
||||||
isActive: boolean;
|
|
||||||
isPaused: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JobItem =
|
export type JobItem =
|
||||||
// Audit
|
// Audit
|
||||||
| { name: JobName.AuditTableCleanup; data?: IBaseJob }
|
| { name: JobName.AuditTableCleanup; data?: IBaseJob }
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ export const newJobRepositoryMock = (): Mocked<RepositoryInterface<JobRepository
|
||||||
empty: vitest.fn(),
|
empty: vitest.fn(),
|
||||||
pause: vitest.fn(),
|
pause: vitest.fn(),
|
||||||
resume: vitest.fn(),
|
resume: vitest.fn(),
|
||||||
|
searchJobs: vitest.fn(),
|
||||||
queue: vitest.fn().mockImplementation(() => Promise.resolve()),
|
queue: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
queueAll: vitest.fn().mockImplementation(() => Promise.resolve()),
|
queueAll: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
getQueueStatus: vitest.fn(),
|
isActive: vitest.fn(),
|
||||||
|
isPaused: vitest.fn(),
|
||||||
getJobCounts: vitest.fn(),
|
getJobCounts: vitest.fn(),
|
||||||
clear: vitest.fn(),
|
clear: vitest.fn(),
|
||||||
waitForQueueCompletion: vitest.fn(),
|
waitForQueueCompletion: vitest.fn(),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from 'src/database';
|
} from 'src/database';
|
||||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.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 { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||||
import { v4, v7 } from 'uuid';
|
import { v4, v7 } from 'uuid';
|
||||||
|
|
@ -139,6 +140,16 @@ const sessionFactory = (session: Partial<Session> = {}) => ({
|
||||||
...session,
|
...session,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
|
||||||
|
active: 0,
|
||||||
|
completed: 0,
|
||||||
|
failed: 0,
|
||||||
|
delayed: 0,
|
||||||
|
waiting: 0,
|
||||||
|
paused: 0,
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
|
||||||
const stackFactory = () => ({
|
const stackFactory = () => ({
|
||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
ownerId: newUuid(),
|
ownerId: newUuid(),
|
||||||
|
|
@ -353,6 +364,7 @@ export const factory = {
|
||||||
library: libraryFactory,
|
library: libraryFactory,
|
||||||
memory: memoryFactory,
|
memory: memoryFactory,
|
||||||
partner: partnerFactory,
|
partner: partnerFactory,
|
||||||
|
queueStatistics: queueStatisticsFactory,
|
||||||
session: sessionFactory,
|
session: sessionFactory,
|
||||||
stack: stackFactory,
|
stack: stackFactory,
|
||||||
user: userFactory,
|
user: userFactory,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Badge from '$lib/elements/Badge.svelte';
|
import Badge from '$lib/elements/Badge.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusDto } from '@immich/sdk';
|
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusLegacyDto } from '@immich/sdk';
|
||||||
import { Icon, IconButton } from '@immich/ui';
|
import { Icon, IconButton } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
subtitle: string | undefined;
|
subtitle: string | undefined;
|
||||||
description: Component | undefined;
|
description: Component | undefined;
|
||||||
statistics: QueueStatisticsDto;
|
statistics: QueueStatisticsDto;
|
||||||
queueStatus: QueueStatusDto;
|
queueStatus: QueueStatusLegacyDto;
|
||||||
icon: string;
|
icon: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
allText: string | undefined;
|
allText: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
QueueCommand,
|
QueueCommand,
|
||||||
type QueueCommandDto,
|
type QueueCommandDto,
|
||||||
QueueName,
|
QueueName,
|
||||||
type QueuesResponseDto,
|
type QueuesResponseLegacyDto,
|
||||||
runQueueCommandLegacy,
|
runQueueCommandLegacy,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { modalManager, toastManager } from '@immich/ui';
|
import { modalManager, toastManager } from '@immich/ui';
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
|
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
jobs: QueuesResponseDto;
|
jobs: QueuesResponseLegacyDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { jobs = $bindable() }: Props = $props();
|
let { jobs = $bindable() }: Props = $props();
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,13 @@
|
||||||
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
|
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
|
||||||
import { asyncTimeout } from '$lib/utils';
|
import { asyncTimeout } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getQueuesLegacy, QueueCommand, QueueName, runQueueCommandLegacy, type QueuesResponseDto } from '@immich/sdk';
|
import {
|
||||||
|
getQueuesLegacy,
|
||||||
|
QueueCommand,
|
||||||
|
QueueName,
|
||||||
|
runQueueCommandLegacy,
|
||||||
|
type QueuesResponseLegacyDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||||
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
|
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
@ -18,7 +24,7 @@
|
||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let jobs: QueuesResponseDto | undefined = $state();
|
let jobs: QueuesResponseLegacyDto | undefined = $state();
|
||||||
|
|
||||||
let running = true;
|
let running = true;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue