refactor: job vs queue naming (#23902)

This commit is contained in:
Jason Rasmussen 2025-11-14 14:42:00 -05:00 committed by GitHub
parent 1200bfad13
commit d784d431d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1356 additions and 1325 deletions

View File

@ -1,4 +1,4 @@
import { JobCommand, JobName, LoginResponseDto, updateConfig } from '@immich/sdk';
import { LoginResponseDto, QueueCommand, QueueName, updateConfig } from '@immich/sdk';
import { cpSync, rmSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
@ -17,28 +17,28 @@ describe('/jobs', () => {
describe('PUT /jobs', () => {
afterEach(async () => {
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
command: QueueCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.FaceDetection, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.FaceDetection, {
command: QueueCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.SmartSearch, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.SmartSearch, {
command: QueueCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.DuplicateDetection, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.DuplicateDetection, {
command: QueueCommand.Resume,
force: false,
});
@ -59,8 +59,8 @@ describe('/jobs', () => {
it('should queue metadata extraction for missing assets', async () => {
const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Pause,
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
command: QueueCommand.Pause,
force: false,
});
@ -77,20 +77,20 @@ describe('/jobs', () => {
expect(asset.exifInfo?.make).toBeNull();
}
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Empty,
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
command: QueueCommand.Empty,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
command: QueueCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Start,
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
command: QueueCommand.Start,
force: false,
});
@ -124,8 +124,8 @@ describe('/jobs', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, path);
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Start,
await utils.queueCommand(admin.accessToken, QueueName.MetadataExtraction, {
command: QueueCommand.Start,
force: false,
});
@ -144,8 +144,8 @@ describe('/jobs', () => {
it('should queue thumbnail extraction for assets missing thumbs', async () => {
const path = `${testAssetDir}/albums/nature/tanners_ridge.jpg`;
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Pause,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Pause,
force: false,
});
@ -153,32 +153,32 @@ describe('/jobs', () => {
assetData: { bytes: await readFile(path), filename: basename(path) },
});
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
expect(assetBefore.thumbhash).toBeNull();
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Empty,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Empty,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Start,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Start,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
const assetAfter = await utils.getAssetInfo(admin.accessToken, id);
expect(assetAfter.thumbhash).not.toBeNull();
@ -193,26 +193,26 @@ describe('/jobs', () => {
assetData: { bytes: await readFile(path), filename: basename(path) },
});
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
cpSync(`${testAssetDir}/albums/nature/notocactus_minimus.jpg`, path);
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Resume,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Resume,
force: false,
});
// This runs the missing thumbnail job
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
command: JobCommand.Start,
await utils.queueCommand(admin.accessToken, QueueName.ThumbnailGeneration, {
command: QueueCommand.Start,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
await utils.waitForQueueFinish(admin.accessToken, QueueName.MetadataExtraction);
await utils.waitForQueueFinish(admin.accessToken, QueueName.ThumbnailGeneration);
const assetAfter = await utils.getAssetInfo(admin.accessToken, id);

View File

@ -1,6 +1,6 @@
import {
JobName,
LoginResponseDto,
QueueName,
createStack,
deleteUserAdmin,
getMyUser,
@ -328,7 +328,7 @@ describe('/admin/users', () => {
{ headers: asBearerAuth(user.accessToken) },
);
await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask);
await utils.waitForQueueFinish(admin.accessToken, QueueName.BackgroundTask);
const { status, body } = await request(app)
.delete(`/admin/users/${user.userId}`)

View File

@ -1,5 +1,4 @@
import {
AllJobStatusResponseDto,
AssetMediaCreateDto,
AssetMediaResponseDto,
AssetResponseDto,
@ -7,11 +6,12 @@ import {
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
JobCommandDto,
JobName,
MetadataSearchDto,
Permission,
PersonCreateDto,
QueueCommandDto,
QueueName,
QueuesResponseDto,
SharedLinkCreateDto,
UpdateLibraryDto,
UserAdminCreateDto,
@ -27,14 +27,14 @@ import {
createStack,
createUserAdmin,
deleteAssets,
getAllJobsStatus,
getAssetInfo,
getConfig,
getConfigDefaults,
getQueuesLegacy,
login,
runQueueCommandLegacy,
scanLibrary,
searchAssets,
sendJobCommand,
setBaseUrl,
signUpAdmin,
tagAssets,
@ -477,8 +477,8 @@ export const utils = {
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
await context.addCookies([
@ -524,13 +524,13 @@ export const utils = {
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
},
isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => {
const queues = await getAllJobsStatus({ headers: asBearerAuth(accessToken) });
isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseDto) => {
const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) });
const jobCounts = queues[queue].jobCounts;
return !jobCounts.active && !jobCounts.waiting;
},
waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => {
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseDto, ms?: number) => {
// eslint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);

View File

@ -149,8 +149,8 @@ Class | Method | HTTP request | Description
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces | Retrieve faces for asset
*FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} | Re-assign a face to another person
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs | Create a manual job
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs | Retrieve queue counts and status
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} | Run jobs
*JobsApi* | [**getQueuesLegacy**](doc//JobsApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
*JobsApi* | [**runQueueCommandLegacy**](doc//JobsApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries | Create a library
*LibrariesApi* | [**deleteLibrary**](doc//LibrariesApi.md#deletelibrary) | **DELETE** /libraries/{id} | Delete a library
*LibrariesApi* | [**getAllLibraries**](doc//LibrariesApi.md#getalllibraries) | **GET** /libraries | Retrieve libraries
@ -318,7 +318,6 @@ Class | Method | HTTP request | Description
- [AlbumsAddAssetsResponseDto](doc//AlbumsAddAssetsResponseDto.md)
- [AlbumsResponse](doc//AlbumsResponse.md)
- [AlbumsUpdate](doc//AlbumsUpdate.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
- [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md)
@ -387,13 +386,8 @@ Class | Method | HTTP request | Description
- [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md)
- [ImageFormat](doc//ImageFormat.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
- [JobCountsDto](doc//JobCountsDto.md)
- [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md)
- [JobStatusDto](doc//JobStatusDto.md)
- [LibraryResponseDto](doc//LibraryResponseDto.md)
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
- [LicenseKeyDto](doc//LicenseKeyDto.md)
@ -452,7 +446,13 @@ Class | Method | HTTP request | Description
- [PlacesResponseDto](doc//PlacesResponseDto.md)
- [PurchaseResponse](doc//PurchaseResponse.md)
- [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueCommand](doc//QueueCommand.md)
- [QueueCommandDto](doc//QueueCommandDto.md)
- [QueueName](doc//QueueName.md)
- [QueueResponseDto](doc//QueueResponseDto.md)
- [QueueStatisticsDto](doc//QueueStatisticsDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- [QueuesResponseDto](doc//QueuesResponseDto.md)
- [RandomSearchDto](doc//RandomSearchDto.md)
- [RatingsResponse](doc//RatingsResponse.md)
- [RatingsUpdate](doc//RatingsUpdate.md)

View File

@ -82,7 +82,6 @@ part 'model/albums_add_assets_dto.dart';
part 'model/albums_add_assets_response_dto.dart';
part 'model/albums_response.dart';
part 'model/albums_update.dart';
part 'model/all_job_status_response_dto.dart';
part 'model/asset_bulk_delete_dto.dart';
part 'model/asset_bulk_update_dto.dart';
part 'model/asset_bulk_upload_check_dto.dart';
@ -151,13 +150,8 @@ part 'model/facial_recognition_config.dart';
part 'model/folders_response.dart';
part 'model/folders_update.dart';
part 'model/image_format.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
part 'model/job_counts_dto.dart';
part 'model/job_create_dto.dart';
part 'model/job_name.dart';
part 'model/job_settings_dto.dart';
part 'model/job_status_dto.dart';
part 'model/library_response_dto.dart';
part 'model/library_stats_response_dto.dart';
part 'model/license_key_dto.dart';
@ -216,7 +210,13 @@ part 'model/pin_code_setup_dto.dart';
part 'model/places_response_dto.dart';
part 'model/purchase_response.dart';
part 'model/purchase_update.dart';
part 'model/queue_command.dart';
part 'model/queue_command_dto.dart';
part 'model/queue_name.dart';
part 'model/queue_response_dto.dart';
part 'model/queue_statistics_dto.dart';
part 'model/queue_status_dto.dart';
part 'model/queues_response_dto.dart';
part 'model/random_search_dto.dart';
part 'model/ratings_response.dart';
part 'model/ratings_update.dart';

View File

@ -69,7 +69,7 @@ class JobsApi {
/// Retrieve the counts of the current queue, as well as the current status.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getAllJobsStatusWithHttpInfo() async {
Future<Response> getQueuesLegacyWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/jobs';
@ -97,8 +97,8 @@ class JobsApi {
/// Retrieve queue counts and status
///
/// Retrieve the counts of the current queue, as well as the current status.
Future<AllJobStatusResponseDto?> getAllJobsStatus() async {
final response = await getAllJobsStatusWithHttpInfo();
Future<QueuesResponseDto?> getQueuesLegacy() async {
final response = await getQueuesLegacyWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -106,7 +106,7 @@ class JobsApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AllJobStatusResponseDto',) as AllJobStatusResponseDto;
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseDto',) as QueuesResponseDto;
}
return null;
@ -120,16 +120,16 @@ class JobsApi {
///
/// Parameters:
///
/// * [JobName] id (required):
/// * [QueueName] name (required):
///
/// * [JobCommandDto] jobCommandDto (required):
Future<Response> sendJobCommandWithHttpInfo(JobName id, JobCommandDto jobCommandDto,) async {
/// * [QueueCommandDto] queueCommandDto (required):
Future<Response> runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/jobs/{id}'
.replaceAll('{id}', id.toString());
final apiPath = r'/jobs/{name}'
.replaceAll('{name}', name.toString());
// ignore: prefer_final_locals
Object? postBody = jobCommandDto;
Object? postBody = queueCommandDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@ -155,11 +155,11 @@ class JobsApi {
///
/// Parameters:
///
/// * [JobName] id (required):
/// * [QueueName] name (required):
///
/// * [JobCommandDto] jobCommandDto (required):
Future<JobStatusDto?> sendJobCommand(JobName id, JobCommandDto jobCommandDto,) async {
final response = await sendJobCommandWithHttpInfo(id, jobCommandDto,);
/// * [QueueCommandDto] queueCommandDto (required):
Future<QueueResponseDto?> runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async {
final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -167,7 +167,7 @@ class JobsApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'JobStatusDto',) as JobStatusDto;
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto;
}
return null;

View File

@ -220,8 +220,6 @@ class ApiClient {
return AlbumsResponse.fromJson(value);
case 'AlbumsUpdate':
return AlbumsUpdate.fromJson(value);
case 'AllJobStatusResponseDto':
return AllJobStatusResponseDto.fromJson(value);
case 'AssetBulkDeleteDto':
return AssetBulkDeleteDto.fromJson(value);
case 'AssetBulkUpdateDto':
@ -358,20 +356,10 @@ class ApiClient {
return FoldersUpdate.fromJson(value);
case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value);
case 'JobCommand':
return JobCommandTypeTransformer().decode(value);
case 'JobCommandDto':
return JobCommandDto.fromJson(value);
case 'JobCountsDto':
return JobCountsDto.fromJson(value);
case 'JobCreateDto':
return JobCreateDto.fromJson(value);
case 'JobName':
return JobNameTypeTransformer().decode(value);
case 'JobSettingsDto':
return JobSettingsDto.fromJson(value);
case 'JobStatusDto':
return JobStatusDto.fromJson(value);
case 'LibraryResponseDto':
return LibraryResponseDto.fromJson(value);
case 'LibraryStatsResponseDto':
@ -488,8 +476,20 @@ class ApiClient {
return PurchaseResponse.fromJson(value);
case 'PurchaseUpdate':
return PurchaseUpdate.fromJson(value);
case 'QueueCommand':
return QueueCommandTypeTransformer().decode(value);
case 'QueueCommandDto':
return QueueCommandDto.fromJson(value);
case 'QueueName':
return QueueNameTypeTransformer().decode(value);
case 'QueueResponseDto':
return QueueResponseDto.fromJson(value);
case 'QueueStatisticsDto':
return QueueStatisticsDto.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
case 'QueuesResponseDto':
return QueuesResponseDto.fromJson(value);
case 'RandomSearchDto':
return RandomSearchDto.fromJson(value);
case 'RatingsResponse':

View File

@ -94,12 +94,6 @@ String parameterToString(dynamic value) {
if (value is ImageFormat) {
return ImageFormatTypeTransformer().encode(value).toString();
}
if (value is JobCommand) {
return JobCommandTypeTransformer().encode(value).toString();
}
if (value is JobName) {
return JobNameTypeTransformer().encode(value).toString();
}
if (value is LogLevel) {
return LogLevelTypeTransformer().encode(value).toString();
}
@ -127,6 +121,12 @@ String parameterToString(dynamic value) {
if (value is Permission) {
return PermissionTypeTransformer().encode(value).toString();
}
if (value is QueueCommand) {
return QueueCommandTypeTransformer().encode(value).toString();
}
if (value is QueueName) {
return QueueNameTypeTransformer().encode(value).toString();
}
if (value is ReactionLevel) {
return ReactionLevelTypeTransformer().encode(value).toString();
}

View File

@ -1,94 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class JobCommand {
/// Instantiate a new enum with the provided [value].
const JobCommand._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const start = JobCommand._(r'start');
static const pause = JobCommand._(r'pause');
static const resume = JobCommand._(r'resume');
static const empty = JobCommand._(r'empty');
static const clearFailed = JobCommand._(r'clear-failed');
/// List of all possible values in this [enum][JobCommand].
static const values = <JobCommand>[
start,
pause,
resume,
empty,
clearFailed,
];
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
static List<JobCommand> listFromJson(dynamic json, {bool growable = false,}) {
final result = <JobCommand>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = JobCommand.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [JobCommand] to String,
/// and [decode] dynamic data back to [JobCommand].
class JobCommandTypeTransformer {
factory JobCommandTypeTransformer() => _instance ??= const JobCommandTypeTransformer._();
const JobCommandTypeTransformer._();
String encode(JobCommand data) => data.value;
/// Decodes a [dynamic value][data] to a JobCommand.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
JobCommand? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'start': return JobCommand.start;
case r'pause': return JobCommand.pause;
case r'resume': return JobCommand.resume;
case r'empty': return JobCommand.empty;
case r'clear-failed': return JobCommand.clearFailed;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [JobCommandTypeTransformer] instance.
static JobCommandTypeTransformer? _instance;
}

View File

@ -1,127 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class JobName {
/// Instantiate a new enum with the provided [value].
const JobName._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const thumbnailGeneration = JobName._(r'thumbnailGeneration');
static const metadataExtraction = JobName._(r'metadataExtraction');
static const videoConversion = JobName._(r'videoConversion');
static const faceDetection = JobName._(r'faceDetection');
static const facialRecognition = JobName._(r'facialRecognition');
static const smartSearch = JobName._(r'smartSearch');
static const duplicateDetection = JobName._(r'duplicateDetection');
static const backgroundTask = JobName._(r'backgroundTask');
static const storageTemplateMigration = JobName._(r'storageTemplateMigration');
static const migration = JobName._(r'migration');
static const search = JobName._(r'search');
static const sidecar = JobName._(r'sidecar');
static const library_ = JobName._(r'library');
static const notifications = JobName._(r'notifications');
static const backupDatabase = JobName._(r'backupDatabase');
static const ocr = JobName._(r'ocr');
/// List of all possible values in this [enum][JobName].
static const values = <JobName>[
thumbnailGeneration,
metadataExtraction,
videoConversion,
faceDetection,
facialRecognition,
smartSearch,
duplicateDetection,
backgroundTask,
storageTemplateMigration,
migration,
search,
sidecar,
library_,
notifications,
backupDatabase,
ocr,
];
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
static List<JobName> listFromJson(dynamic json, {bool growable = false,}) {
final result = <JobName>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = JobName.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [JobName] to String,
/// and [decode] dynamic data back to [JobName].
class JobNameTypeTransformer {
factory JobNameTypeTransformer() => _instance ??= const JobNameTypeTransformer._();
const JobNameTypeTransformer._();
String encode(JobName data) => data.value;
/// Decodes a [dynamic value][data] to a JobName.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
JobName? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'thumbnailGeneration': return JobName.thumbnailGeneration;
case r'metadataExtraction': return JobName.metadataExtraction;
case r'videoConversion': return JobName.videoConversion;
case r'faceDetection': return JobName.faceDetection;
case r'facialRecognition': return JobName.facialRecognition;
case r'smartSearch': return JobName.smartSearch;
case r'duplicateDetection': return JobName.duplicateDetection;
case r'backgroundTask': return JobName.backgroundTask;
case r'storageTemplateMigration': return JobName.storageTemplateMigration;
case r'migration': return JobName.migration;
case r'search': return JobName.search;
case r'sidecar': return JobName.sidecar;
case r'library': return JobName.library_;
case r'notifications': return JobName.notifications;
case r'backupDatabase': return JobName.backupDatabase;
case r'ocr': return JobName.ocr;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [JobNameTypeTransformer] instance.
static JobNameTypeTransformer? _instance;
}

View File

@ -0,0 +1,94 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueCommand {
/// Instantiate a new enum with the provided [value].
const QueueCommand._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const start = QueueCommand._(r'start');
static const pause = QueueCommand._(r'pause');
static const resume = QueueCommand._(r'resume');
static const empty = QueueCommand._(r'empty');
static const clearFailed = QueueCommand._(r'clear-failed');
/// List of all possible values in this [enum][QueueCommand].
static const values = <QueueCommand>[
start,
pause,
resume,
empty,
clearFailed,
];
static QueueCommand? fromJson(dynamic value) => QueueCommandTypeTransformer().decode(value);
static List<QueueCommand> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueCommand>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueCommand.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [QueueCommand] to String,
/// and [decode] dynamic data back to [QueueCommand].
class QueueCommandTypeTransformer {
factory QueueCommandTypeTransformer() => _instance ??= const QueueCommandTypeTransformer._();
const QueueCommandTypeTransformer._();
String encode(QueueCommand data) => data.value;
/// Decodes a [dynamic value][data] to a QueueCommand.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
QueueCommand? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'start': return QueueCommand.start;
case r'pause': return QueueCommand.pause;
case r'resume': return QueueCommand.resume;
case r'empty': return QueueCommand.empty;
case r'clear-failed': return QueueCommand.clearFailed;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [QueueCommandTypeTransformer] instance.
static QueueCommandTypeTransformer? _instance;
}

View File

@ -10,14 +10,14 @@
part of openapi.api;
class JobCommandDto {
/// Returns a new [JobCommandDto] instance.
JobCommandDto({
class QueueCommandDto {
/// Returns a new [QueueCommandDto] instance.
QueueCommandDto({
required this.command,
this.force,
});
JobCommand command;
QueueCommand command;
///
/// Please note: This property should have been non-nullable! Since the specification file
@ -28,7 +28,7 @@ class JobCommandDto {
bool? force;
@override
bool operator ==(Object other) => identical(this, other) || other is JobCommandDto &&
bool operator ==(Object other) => identical(this, other) || other is QueueCommandDto &&
other.command == command &&
other.force == force;
@ -39,7 +39,7 @@ class JobCommandDto {
(force == null ? 0 : force!.hashCode);
@override
String toString() => 'JobCommandDto[command=$command, force=$force]';
String toString() => 'QueueCommandDto[command=$command, force=$force]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -52,27 +52,27 @@ class JobCommandDto {
return json;
}
/// Returns a new [JobCommandDto] instance and imports its values from
/// Returns a new [QueueCommandDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static JobCommandDto? fromJson(dynamic value) {
upgradeDto(value, "JobCommandDto");
static QueueCommandDto? fromJson(dynamic value) {
upgradeDto(value, "QueueCommandDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return JobCommandDto(
command: JobCommand.fromJson(json[r'command'])!,
return QueueCommandDto(
command: QueueCommand.fromJson(json[r'command'])!,
force: mapValueOfType<bool>(json, r'force'),
);
}
return null;
}
static List<JobCommandDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <JobCommandDto>[];
static List<QueueCommandDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueCommandDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = JobCommandDto.fromJson(row);
final value = QueueCommandDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -81,12 +81,12 @@ class JobCommandDto {
return result.toList(growable: growable);
}
static Map<String, JobCommandDto> mapFromJson(dynamic json) {
final map = <String, JobCommandDto>{};
static Map<String, QueueCommandDto> mapFromJson(dynamic json) {
final map = <String, QueueCommandDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = JobCommandDto.fromJson(entry.value);
final value = QueueCommandDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -95,14 +95,14 @@ class JobCommandDto {
return map;
}
// maps a json object with a list of JobCommandDto-objects as value to a dart map
static Map<String, List<JobCommandDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<JobCommandDto>>{};
// maps a json object with a list of QueueCommandDto-objects as value to a dart map
static Map<String, List<QueueCommandDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueCommandDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = JobCommandDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = QueueCommandDto.listFromJson(entry.value, growable: growable,);
}
}
return map;

127
mobile/openapi/lib/model/queue_name.dart generated Normal file
View File

@ -0,0 +1,127 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class QueueName {
/// Instantiate a new enum with the provided [value].
const QueueName._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const thumbnailGeneration = QueueName._(r'thumbnailGeneration');
static const metadataExtraction = QueueName._(r'metadataExtraction');
static const videoConversion = QueueName._(r'videoConversion');
static const faceDetection = QueueName._(r'faceDetection');
static const facialRecognition = QueueName._(r'facialRecognition');
static const smartSearch = QueueName._(r'smartSearch');
static const duplicateDetection = QueueName._(r'duplicateDetection');
static const backgroundTask = QueueName._(r'backgroundTask');
static const storageTemplateMigration = QueueName._(r'storageTemplateMigration');
static const migration = QueueName._(r'migration');
static const search = QueueName._(r'search');
static const sidecar = QueueName._(r'sidecar');
static const library_ = QueueName._(r'library');
static const notifications = QueueName._(r'notifications');
static const backupDatabase = QueueName._(r'backupDatabase');
static const ocr = QueueName._(r'ocr');
/// List of all possible values in this [enum][QueueName].
static const values = <QueueName>[
thumbnailGeneration,
metadataExtraction,
videoConversion,
faceDetection,
facialRecognition,
smartSearch,
duplicateDetection,
backgroundTask,
storageTemplateMigration,
migration,
search,
sidecar,
library_,
notifications,
backupDatabase,
ocr,
];
static QueueName? fromJson(dynamic value) => QueueNameTypeTransformer().decode(value);
static List<QueueName> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueName>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = QueueName.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [QueueName] to String,
/// and [decode] dynamic data back to [QueueName].
class QueueNameTypeTransformer {
factory QueueNameTypeTransformer() => _instance ??= const QueueNameTypeTransformer._();
const QueueNameTypeTransformer._();
String encode(QueueName data) => data.value;
/// Decodes a [dynamic value][data] to a QueueName.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
QueueName? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'thumbnailGeneration': return QueueName.thumbnailGeneration;
case r'metadataExtraction': return QueueName.metadataExtraction;
case r'videoConversion': return QueueName.videoConversion;
case r'faceDetection': return QueueName.faceDetection;
case r'facialRecognition': return QueueName.facialRecognition;
case r'smartSearch': return QueueName.smartSearch;
case r'duplicateDetection': return QueueName.duplicateDetection;
case r'backgroundTask': return QueueName.backgroundTask;
case r'storageTemplateMigration': return QueueName.storageTemplateMigration;
case r'migration': return QueueName.migration;
case r'search': return QueueName.search;
case r'sidecar': return QueueName.sidecar;
case r'library': return QueueName.library_;
case r'notifications': return QueueName.notifications;
case r'backupDatabase': return QueueName.backupDatabase;
case r'ocr': return QueueName.ocr;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [QueueNameTypeTransformer] instance.
static QueueNameTypeTransformer? _instance;
}

View File

@ -10,19 +10,19 @@
part of openapi.api;
class JobStatusDto {
/// Returns a new [JobStatusDto] instance.
JobStatusDto({
class QueueResponseDto {
/// Returns a new [QueueResponseDto] instance.
QueueResponseDto({
required this.jobCounts,
required this.queueStatus,
});
JobCountsDto jobCounts;
QueueStatisticsDto jobCounts;
QueueStatusDto queueStatus;
@override
bool operator ==(Object other) => identical(this, other) || other is JobStatusDto &&
bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto &&
other.jobCounts == jobCounts &&
other.queueStatus == queueStatus;
@ -33,7 +33,7 @@ class JobStatusDto {
(queueStatus.hashCode);
@override
String toString() => 'JobStatusDto[jobCounts=$jobCounts, queueStatus=$queueStatus]';
String toString() => 'QueueResponseDto[jobCounts=$jobCounts, queueStatus=$queueStatus]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -42,27 +42,27 @@ class JobStatusDto {
return json;
}
/// Returns a new [JobStatusDto] instance and imports its values from
/// Returns a new [QueueResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static JobStatusDto? fromJson(dynamic value) {
upgradeDto(value, "JobStatusDto");
static QueueResponseDto? fromJson(dynamic value) {
upgradeDto(value, "QueueResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return JobStatusDto(
jobCounts: JobCountsDto.fromJson(json[r'jobCounts'])!,
return QueueResponseDto(
jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!,
queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!,
);
}
return null;
}
static List<JobStatusDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <JobStatusDto>[];
static List<QueueResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = JobStatusDto.fromJson(row);
final value = QueueResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -71,12 +71,12 @@ class JobStatusDto {
return result.toList(growable: growable);
}
static Map<String, JobStatusDto> mapFromJson(dynamic json) {
final map = <String, JobStatusDto>{};
static Map<String, QueueResponseDto> mapFromJson(dynamic json) {
final map = <String, QueueResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = JobStatusDto.fromJson(entry.value);
final value = QueueResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -85,14 +85,14 @@ class JobStatusDto {
return map;
}
// maps a json object with a list of JobStatusDto-objects as value to a dart map
static Map<String, List<JobStatusDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<JobStatusDto>>{};
// maps a json object with a list of QueueResponseDto-objects as value to a dart map
static Map<String, List<QueueResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = JobStatusDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = QueueResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;

View File

@ -10,9 +10,9 @@
part of openapi.api;
class JobCountsDto {
/// Returns a new [JobCountsDto] instance.
JobCountsDto({
class QueueStatisticsDto {
/// Returns a new [QueueStatisticsDto] instance.
QueueStatisticsDto({
required this.active,
required this.completed,
required this.delayed,
@ -34,7 +34,7 @@ class JobCountsDto {
int waiting;
@override
bool operator ==(Object other) => identical(this, other) || other is JobCountsDto &&
bool operator ==(Object other) => identical(this, other) || other is QueueStatisticsDto &&
other.active == active &&
other.completed == completed &&
other.delayed == delayed &&
@ -53,7 +53,7 @@ class JobCountsDto {
(waiting.hashCode);
@override
String toString() => 'JobCountsDto[active=$active, completed=$completed, delayed=$delayed, failed=$failed, paused=$paused, waiting=$waiting]';
String toString() => 'QueueStatisticsDto[active=$active, completed=$completed, delayed=$delayed, failed=$failed, paused=$paused, waiting=$waiting]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -66,15 +66,15 @@ class JobCountsDto {
return json;
}
/// Returns a new [JobCountsDto] instance and imports its values from
/// Returns a new [QueueStatisticsDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static JobCountsDto? fromJson(dynamic value) {
upgradeDto(value, "JobCountsDto");
static QueueStatisticsDto? fromJson(dynamic value) {
upgradeDto(value, "QueueStatisticsDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return JobCountsDto(
return QueueStatisticsDto(
active: mapValueOfType<int>(json, r'active')!,
completed: mapValueOfType<int>(json, r'completed')!,
delayed: mapValueOfType<int>(json, r'delayed')!,
@ -86,11 +86,11 @@ class JobCountsDto {
return null;
}
static List<JobCountsDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <JobCountsDto>[];
static List<QueueStatisticsDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueueStatisticsDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = JobCountsDto.fromJson(row);
final value = QueueStatisticsDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -99,12 +99,12 @@ class JobCountsDto {
return result.toList(growable: growable);
}
static Map<String, JobCountsDto> mapFromJson(dynamic json) {
final map = <String, JobCountsDto>{};
static Map<String, QueueStatisticsDto> mapFromJson(dynamic json) {
final map = <String, QueueStatisticsDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = JobCountsDto.fromJson(entry.value);
final value = QueueStatisticsDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -113,14 +113,14 @@ class JobCountsDto {
return map;
}
// maps a json object with a list of JobCountsDto-objects as value to a dart map
static Map<String, List<JobCountsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<JobCountsDto>>{};
// maps a json object with a list of QueueStatisticsDto-objects as value to a dart map
static Map<String, List<QueueStatisticsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueueStatisticsDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = JobCountsDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = QueueStatisticsDto.listFromJson(entry.value, growable: growable,);
}
}
return map;

View File

@ -10,9 +10,9 @@
part of openapi.api;
class AllJobStatusResponseDto {
/// Returns a new [AllJobStatusResponseDto] instance.
AllJobStatusResponseDto({
class QueuesResponseDto {
/// Returns a new [QueuesResponseDto] instance.
QueuesResponseDto({
required this.backgroundTask,
required this.backupDatabase,
required this.duplicateDetection,
@ -31,40 +31,40 @@ class AllJobStatusResponseDto {
required this.videoConversion,
});
JobStatusDto backgroundTask;
QueueResponseDto backgroundTask;
JobStatusDto backupDatabase;
QueueResponseDto backupDatabase;
JobStatusDto duplicateDetection;
QueueResponseDto duplicateDetection;
JobStatusDto faceDetection;
QueueResponseDto faceDetection;
JobStatusDto facialRecognition;
QueueResponseDto facialRecognition;
JobStatusDto library_;
QueueResponseDto library_;
JobStatusDto metadataExtraction;
QueueResponseDto metadataExtraction;
JobStatusDto migration;
QueueResponseDto migration;
JobStatusDto notifications;
QueueResponseDto notifications;
JobStatusDto ocr;
QueueResponseDto ocr;
JobStatusDto search;
QueueResponseDto search;
JobStatusDto sidecar;
QueueResponseDto sidecar;
JobStatusDto smartSearch;
QueueResponseDto smartSearch;
JobStatusDto storageTemplateMigration;
QueueResponseDto storageTemplateMigration;
JobStatusDto thumbnailGeneration;
QueueResponseDto thumbnailGeneration;
JobStatusDto videoConversion;
QueueResponseDto videoConversion;
@override
bool operator ==(Object other) => identical(this, other) || other is AllJobStatusResponseDto &&
bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto &&
other.backgroundTask == backgroundTask &&
other.backupDatabase == backupDatabase &&
other.duplicateDetection == duplicateDetection &&
@ -103,7 +103,7 @@ class AllJobStatusResponseDto {
(videoConversion.hashCode);
@override
String toString() => 'AllJobStatusResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
String toString() => 'QueuesResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -126,41 +126,41 @@ class AllJobStatusResponseDto {
return json;
}
/// Returns a new [AllJobStatusResponseDto] instance and imports its values from
/// Returns a new [QueuesResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AllJobStatusResponseDto? fromJson(dynamic value) {
upgradeDto(value, "AllJobStatusResponseDto");
static QueuesResponseDto? fromJson(dynamic value) {
upgradeDto(value, "QueuesResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AllJobStatusResponseDto(
backgroundTask: JobStatusDto.fromJson(json[r'backgroundTask'])!,
backupDatabase: JobStatusDto.fromJson(json[r'backupDatabase'])!,
duplicateDetection: JobStatusDto.fromJson(json[r'duplicateDetection'])!,
faceDetection: JobStatusDto.fromJson(json[r'faceDetection'])!,
facialRecognition: JobStatusDto.fromJson(json[r'facialRecognition'])!,
library_: JobStatusDto.fromJson(json[r'library'])!,
metadataExtraction: JobStatusDto.fromJson(json[r'metadataExtraction'])!,
migration: JobStatusDto.fromJson(json[r'migration'])!,
notifications: JobStatusDto.fromJson(json[r'notifications'])!,
ocr: JobStatusDto.fromJson(json[r'ocr'])!,
search: JobStatusDto.fromJson(json[r'search'])!,
sidecar: JobStatusDto.fromJson(json[r'sidecar'])!,
smartSearch: JobStatusDto.fromJson(json[r'smartSearch'])!,
storageTemplateMigration: JobStatusDto.fromJson(json[r'storageTemplateMigration'])!,
thumbnailGeneration: JobStatusDto.fromJson(json[r'thumbnailGeneration'])!,
videoConversion: JobStatusDto.fromJson(json[r'videoConversion'])!,
return QueuesResponseDto(
backgroundTask: QueueResponseDto.fromJson(json[r'backgroundTask'])!,
backupDatabase: QueueResponseDto.fromJson(json[r'backupDatabase'])!,
duplicateDetection: QueueResponseDto.fromJson(json[r'duplicateDetection'])!,
faceDetection: QueueResponseDto.fromJson(json[r'faceDetection'])!,
facialRecognition: QueueResponseDto.fromJson(json[r'facialRecognition'])!,
library_: QueueResponseDto.fromJson(json[r'library'])!,
metadataExtraction: QueueResponseDto.fromJson(json[r'metadataExtraction'])!,
migration: QueueResponseDto.fromJson(json[r'migration'])!,
notifications: QueueResponseDto.fromJson(json[r'notifications'])!,
ocr: QueueResponseDto.fromJson(json[r'ocr'])!,
search: QueueResponseDto.fromJson(json[r'search'])!,
sidecar: QueueResponseDto.fromJson(json[r'sidecar'])!,
smartSearch: QueueResponseDto.fromJson(json[r'smartSearch'])!,
storageTemplateMigration: QueueResponseDto.fromJson(json[r'storageTemplateMigration'])!,
thumbnailGeneration: QueueResponseDto.fromJson(json[r'thumbnailGeneration'])!,
videoConversion: QueueResponseDto.fromJson(json[r'videoConversion'])!,
);
}
return null;
}
static List<AllJobStatusResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AllJobStatusResponseDto>[];
static List<QueuesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <QueuesResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AllJobStatusResponseDto.fromJson(row);
final value = QueuesResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
@ -169,12 +169,12 @@ class AllJobStatusResponseDto {
return result.toList(growable: growable);
}
static Map<String, AllJobStatusResponseDto> mapFromJson(dynamic json) {
final map = <String, AllJobStatusResponseDto>{};
static Map<String, QueuesResponseDto> mapFromJson(dynamic json) {
final map = <String, QueuesResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AllJobStatusResponseDto.fromJson(entry.value);
final value = QueuesResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@ -183,14 +183,14 @@ class AllJobStatusResponseDto {
return map;
}
// maps a json object with a list of AllJobStatusResponseDto-objects as value to a dart map
static Map<String, List<AllJobStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AllJobStatusResponseDto>>{};
// maps a json object with a list of QueuesResponseDto-objects as value to a dart map
static Map<String, List<QueuesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<QueuesResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AllJobStatusResponseDto.listFromJson(entry.value, growable: growable,);
map[entry.key] = QueuesResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;

View File

@ -4836,14 +4836,14 @@
"/jobs": {
"get": {
"description": "Retrieve the counts of the current queue, as well as the current status.",
"operationId": "getAllJobsStatus",
"operationId": "getQueuesLegacy",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AllJobStatusResponseDto"
"$ref": "#/components/schemas/QueuesResponseDto"
}
}
},
@ -4936,17 +4936,17 @@
"x-immich-state": "Stable"
}
},
"/jobs/{id}": {
"/jobs/{name}": {
"put": {
"description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.",
"operationId": "sendJobCommand",
"operationId": "runQueueCommandLegacy",
"parameters": [
{
"name": "id",
"name": "name",
"required": true,
"in": "path",
"schema": {
"$ref": "#/components/schemas/JobName"
"$ref": "#/components/schemas/QueueName"
}
}
],
@ -4954,7 +4954,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JobCommandDto"
"$ref": "#/components/schemas/QueueCommandDto"
}
}
},
@ -4965,7 +4965,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/JobStatusDto"
"$ref": "#/components/schemas/QueueResponseDto"
}
}
},
@ -14084,77 +14084,6 @@
},
"type": "object"
},
"AllJobStatusResponseDto": {
"properties": {
"backgroundTask": {
"$ref": "#/components/schemas/JobStatusDto"
},
"backupDatabase": {
"$ref": "#/components/schemas/JobStatusDto"
},
"duplicateDetection": {
"$ref": "#/components/schemas/JobStatusDto"
},
"faceDetection": {
"$ref": "#/components/schemas/JobStatusDto"
},
"facialRecognition": {
"$ref": "#/components/schemas/JobStatusDto"
},
"library": {
"$ref": "#/components/schemas/JobStatusDto"
},
"metadataExtraction": {
"$ref": "#/components/schemas/JobStatusDto"
},
"migration": {
"$ref": "#/components/schemas/JobStatusDto"
},
"notifications": {
"$ref": "#/components/schemas/JobStatusDto"
},
"ocr": {
"$ref": "#/components/schemas/JobStatusDto"
},
"search": {
"$ref": "#/components/schemas/JobStatusDto"
},
"sidecar": {
"$ref": "#/components/schemas/JobStatusDto"
},
"smartSearch": {
"$ref": "#/components/schemas/JobStatusDto"
},
"storageTemplateMigration": {
"$ref": "#/components/schemas/JobStatusDto"
},
"thumbnailGeneration": {
"$ref": "#/components/schemas/JobStatusDto"
},
"videoConversion": {
"$ref": "#/components/schemas/JobStatusDto"
}
},
"required": [
"backgroundTask",
"backupDatabase",
"duplicateDetection",
"faceDetection",
"facialRecognition",
"library",
"metadataExtraction",
"migration",
"notifications",
"ocr",
"search",
"sidecar",
"smartSearch",
"storageTemplateMigration",
"thumbnailGeneration",
"videoConversion"
],
"type": "object"
},
"AssetBulkDeleteDto": {
"properties": {
"force": {
@ -15866,65 +15795,6 @@
],
"type": "string"
},
"JobCommand": {
"enum": [
"start",
"pause",
"resume",
"empty",
"clear-failed"
],
"type": "string"
},
"JobCommandDto": {
"properties": {
"command": {
"allOf": [
{
"$ref": "#/components/schemas/JobCommand"
}
]
},
"force": {
"type": "boolean"
}
},
"required": [
"command"
],
"type": "object"
},
"JobCountsDto": {
"properties": {
"active": {
"type": "integer"
},
"completed": {
"type": "integer"
},
"delayed": {
"type": "integer"
},
"failed": {
"type": "integer"
},
"paused": {
"type": "integer"
},
"waiting": {
"type": "integer"
}
},
"required": [
"active",
"completed",
"delayed",
"failed",
"paused",
"waiting"
],
"type": "object"
},
"JobCreateDto": {
"properties": {
"name": {
@ -15940,27 +15810,6 @@
],
"type": "object"
},
"JobName": {
"enum": [
"thumbnailGeneration",
"metadataExtraction",
"videoConversion",
"faceDetection",
"facialRecognition",
"smartSearch",
"duplicateDetection",
"backgroundTask",
"storageTemplateMigration",
"migration",
"search",
"sidecar",
"library",
"notifications",
"backupDatabase",
"ocr"
],
"type": "string"
},
"JobSettingsDto": {
"properties": {
"concurrency": {
@ -15973,21 +15822,6 @@
],
"type": "object"
},
"JobStatusDto": {
"properties": {
"jobCounts": {
"$ref": "#/components/schemas/JobCountsDto"
},
"queueStatus": {
"$ref": "#/components/schemas/QueueStatusDto"
}
},
"required": [
"jobCounts",
"queueStatus"
],
"type": "object"
},
"LibraryResponseDto": {
"properties": {
"assetCount": {
@ -17559,6 +17393,101 @@
},
"type": "object"
},
"QueueCommand": {
"enum": [
"start",
"pause",
"resume",
"empty",
"clear-failed"
],
"type": "string"
},
"QueueCommandDto": {
"properties": {
"command": {
"allOf": [
{
"$ref": "#/components/schemas/QueueCommand"
}
]
},
"force": {
"type": "boolean"
}
},
"required": [
"command"
],
"type": "object"
},
"QueueName": {
"enum": [
"thumbnailGeneration",
"metadataExtraction",
"videoConversion",
"faceDetection",
"facialRecognition",
"smartSearch",
"duplicateDetection",
"backgroundTask",
"storageTemplateMigration",
"migration",
"search",
"sidecar",
"library",
"notifications",
"backupDatabase",
"ocr"
],
"type": "string"
},
"QueueResponseDto": {
"properties": {
"jobCounts": {
"$ref": "#/components/schemas/QueueStatisticsDto"
},
"queueStatus": {
"$ref": "#/components/schemas/QueueStatusDto"
}
},
"required": [
"jobCounts",
"queueStatus"
],
"type": "object"
},
"QueueStatisticsDto": {
"properties": {
"active": {
"type": "integer"
},
"completed": {
"type": "integer"
},
"delayed": {
"type": "integer"
},
"failed": {
"type": "integer"
},
"paused": {
"type": "integer"
},
"waiting": {
"type": "integer"
}
},
"required": [
"active",
"completed",
"delayed",
"failed",
"paused",
"waiting"
],
"type": "object"
},
"QueueStatusDto": {
"properties": {
"isActive": {
@ -17574,6 +17503,77 @@
],
"type": "object"
},
"QueuesResponseDto": {
"properties": {
"backgroundTask": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"backupDatabase": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"duplicateDetection": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"faceDetection": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"facialRecognition": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"library": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"metadataExtraction": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"migration": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"notifications": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"ocr": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"search": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"sidecar": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"smartSearch": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"storageTemplateMigration": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"thumbnailGeneration": {
"$ref": "#/components/schemas/QueueResponseDto"
},
"videoConversion": {
"$ref": "#/components/schemas/QueueResponseDto"
}
},
"required": [
"backgroundTask",
"backupDatabase",
"duplicateDetection",
"faceDetection",
"facialRecognition",
"library",
"metadataExtraction",
"migration",
"notifications",
"ocr",
"search",
"sidecar",
"smartSearch",
"storageTemplateMigration",
"thumbnailGeneration",
"videoConversion"
],
"type": "object"
},
"RandomSearchDto": {
"properties": {
"albumIds": {

View File

@ -699,7 +699,7 @@ export type AssetFaceDeleteDto = {
export type FaceDto = {
id: string;
};
export type JobCountsDto = {
export type QueueStatisticsDto = {
active: number;
completed: number;
delayed: number;
@ -711,33 +711,33 @@ export type QueueStatusDto = {
isActive: boolean;
isPaused: boolean;
};
export type JobStatusDto = {
jobCounts: JobCountsDto;
export type QueueResponseDto = {
jobCounts: QueueStatisticsDto;
queueStatus: QueueStatusDto;
};
export type AllJobStatusResponseDto = {
backgroundTask: JobStatusDto;
backupDatabase: JobStatusDto;
duplicateDetection: JobStatusDto;
faceDetection: JobStatusDto;
facialRecognition: JobStatusDto;
library: JobStatusDto;
metadataExtraction: JobStatusDto;
migration: JobStatusDto;
notifications: JobStatusDto;
ocr: JobStatusDto;
search: JobStatusDto;
sidecar: JobStatusDto;
smartSearch: JobStatusDto;
storageTemplateMigration: JobStatusDto;
thumbnailGeneration: JobStatusDto;
videoConversion: JobStatusDto;
export type QueuesResponseDto = {
backgroundTask: QueueResponseDto;
backupDatabase: QueueResponseDto;
duplicateDetection: QueueResponseDto;
faceDetection: QueueResponseDto;
facialRecognition: QueueResponseDto;
library: QueueResponseDto;
metadataExtraction: QueueResponseDto;
migration: QueueResponseDto;
notifications: QueueResponseDto;
ocr: QueueResponseDto;
search: QueueResponseDto;
sidecar: QueueResponseDto;
smartSearch: QueueResponseDto;
storageTemplateMigration: QueueResponseDto;
thumbnailGeneration: QueueResponseDto;
videoConversion: QueueResponseDto;
};
export type JobCreateDto = {
name: ManualJobName;
};
export type JobCommandDto = {
command: JobCommand;
export type QueueCommandDto = {
command: QueueCommand;
force?: boolean;
};
export type LibraryResponseDto = {
@ -2805,10 +2805,10 @@ export function reassignFacesById({ id, faceDto }: {
/**
* Retrieve queue counts and status
*/
export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) {
export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AllJobStatusResponseDto;
data: QueuesResponseDto;
}>("/jobs", {
...opts
}));
@ -2828,17 +2828,17 @@ export function createJob({ jobCreateDto }: {
/**
* Run jobs
*/
export function sendJobCommand({ id, jobCommandDto }: {
id: JobName;
jobCommandDto: JobCommandDto;
export function runQueueCommandLegacy({ name, queueCommandDto }: {
name: QueueName;
queueCommandDto: QueueCommandDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: JobStatusDto;
}>(`/jobs/${encodeURIComponent(id)}`, oazapfts.json({
data: QueueResponseDto;
}>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({
...opts,
method: "PUT",
body: jobCommandDto
body: queueCommandDto
})));
}
/**
@ -5067,7 +5067,7 @@ export enum ManualJobName {
MemoryCreate = "memory-create",
BackupDatabase = "backup-database"
}
export enum JobName {
export enum QueueName {
ThumbnailGeneration = "thumbnailGeneration",
MetadataExtraction = "metadataExtraction",
VideoConversion = "videoConversion",
@ -5085,7 +5085,7 @@ export enum JobName {
BackupDatabase = "backupDatabase",
Ocr = "ocr"
}
export enum JobCommand {
export enum QueueCommand {
Start = "start",
Pause = "pause",
Resume = "resume",

View File

@ -23,7 +23,7 @@ import { WebsocketRepository } from 'src/repositories/websocket.repository';
import { services } from 'src/services';
import { AuthService } from 'src/services/auth.service';
import { CliService } from 'src/services/cli.service';
import { JobService } from 'src/services/job.service';
import { QueueService } from 'src/services/queue.service';
import { getKyselyConfig } from 'src/utils/database';
const common = [...repositories, ...services, GlobalExceptionFilter];
@ -52,11 +52,11 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
constructor(
@Inject(IWorker) private worker: ImmichWorker,
logger: LoggingRepository,
private eventRepository: EventRepository,
private websocketRepository: WebsocketRepository,
private jobService: JobService,
private telemetryRepository: TelemetryRepository,
private authService: AuthService,
private eventRepository: EventRepository,
private queueService: QueueService,
private telemetryRepository: TelemetryRepository,
private websocketRepository: WebsocketRepository,
) {
logger.setAppName(this.worker);
}
@ -64,7 +64,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
this.telemetryRepository.setup({ repositories });
this.jobService.setServices(services);
this.queueService.setServices(services);
this.websocketRepository.setAuthFn(async (client) =>
this.authService.authenticate({

View File

@ -1,15 +1,20 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
import { JobCreateDto } from 'src/dtos/job.dto';
import { QueueCommandDto, QueueNameParamDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto';
import { ApiTag, Permission } from 'src/enum';
import { Authenticated } from 'src/middleware/auth.guard';
import { JobService } from 'src/services/job.service';
import { QueueService } from 'src/services/queue.service';
@ApiTags(ApiTag.Jobs)
@Controller('jobs')
export class JobController {
constructor(private service: JobService) {}
constructor(
private service: JobService,
private queueService: QueueService,
) {}
@Get()
@Authenticated({ permission: Permission.JobRead, admin: true })
@ -18,8 +23,8 @@ export class JobController {
description: 'Retrieve the counts of the current queue, as well as the current status.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
})
getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
return this.service.getAllJobsStatus();
getQueuesLegacy(): Promise<QueuesResponseDto> {
return this.queueService.getAll();
}
@Post()
@ -35,7 +40,7 @@ export class JobController {
return this.service.create(dto);
}
@Put(':id')
@Put(':name')
@Authenticated({ permission: Permission.JobCreate, admin: true })
@Endpoint({
summary: 'Run jobs',
@ -43,7 +48,7 @@ export class JobController {
'Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
})
sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
return this.service.handleCommand(id, dto);
runQueueCommandLegacy(@Param() { name }: QueueNameParamDto, @Body() dto: QueueCommandDto): Promise<QueueResponseDto> {
return this.queueService.runCommand(name, dto);
}
}

View File

@ -1,99 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { JobCommand, ManualJobName, QueueName } from 'src/enum';
import { ValidateBoolean, ValidateEnum } from 'src/validation';
export class JobIdParamDto {
@ValidateEnum({ enum: QueueName, name: 'JobName' })
id!: QueueName;
}
export class JobCommandDto {
@ValidateEnum({ enum: JobCommand, name: 'JobCommand' })
command!: JobCommand;
@ValidateBoolean({ optional: true })
force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
}
import { ManualJobName } from 'src/enum';
import { ValidateEnum } from 'src/validation';
export class JobCreateDto {
@ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' })
name!: ManualJobName;
}
export class JobCountsDto {
@ApiProperty({ type: 'integer' })
active!: number;
@ApiProperty({ type: 'integer' })
completed!: number;
@ApiProperty({ type: 'integer' })
failed!: number;
@ApiProperty({ type: 'integer' })
delayed!: number;
@ApiProperty({ type: 'integer' })
waiting!: number;
@ApiProperty({ type: 'integer' })
paused!: number;
}
export class QueueStatusDto {
isActive!: boolean;
isPaused!: boolean;
}
export class JobStatusDto {
@ApiProperty({ type: JobCountsDto })
jobCounts!: JobCountsDto;
@ApiProperty({ type: QueueStatusDto })
queueStatus!: QueueStatusDto;
}
export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto> {
@ApiProperty({ type: JobStatusDto })
[QueueName.ThumbnailGeneration]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.MetadataExtraction]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.VideoConversion]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.SmartSearch]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.StorageTemplateMigration]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.Migration]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.BackgroundTask]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.Search]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.DuplicateDetection]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.FaceDetection]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.FacialRecognition]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.Sidecar]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.Library]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.Notification]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.BackupDatabase]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto })
[QueueName.Ocr]!: JobStatusDto;
}

View File

@ -0,0 +1,94 @@
import { ApiProperty } from '@nestjs/swagger';
import { QueueCommand, QueueName } from 'src/enum';
import { ValidateBoolean, ValidateEnum } from 'src/validation';
export class QueueNameParamDto {
@ValidateEnum({ enum: QueueName, name: 'QueueName' })
name!: QueueName;
}
export class QueueCommandDto {
@ValidateEnum({ enum: QueueCommand, name: 'QueueCommand' })
command!: QueueCommand;
@ValidateBoolean({ optional: true })
force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
}
export class QueueStatisticsDto {
@ApiProperty({ type: 'integer' })
active!: number;
@ApiProperty({ type: 'integer' })
completed!: number;
@ApiProperty({ type: 'integer' })
failed!: number;
@ApiProperty({ type: 'integer' })
delayed!: number;
@ApiProperty({ type: 'integer' })
waiting!: number;
@ApiProperty({ type: 'integer' })
paused!: number;
}
export class QueueStatusDto {
isActive!: boolean;
isPaused!: boolean;
}
export class QueueResponseDto {
@ApiProperty({ type: QueueStatisticsDto })
jobCounts!: QueueStatisticsDto;
@ApiProperty({ type: QueueStatusDto })
queueStatus!: QueueStatusDto;
}
export class QueuesResponseDto implements Record<QueueName, QueueResponseDto> {
@ApiProperty({ type: QueueResponseDto })
[QueueName.ThumbnailGeneration]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.MetadataExtraction]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.VideoConversion]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.SmartSearch]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.StorageTemplateMigration]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Migration]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.BackgroundTask]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Search]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.DuplicateDetection]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.FaceDetection]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.FacialRecognition]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Sidecar]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Library]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Notification]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.BackupDatabase]!: QueueResponseDto;
@ApiProperty({ type: QueueResponseDto })
[QueueName.Ocr]!: QueueResponseDto;
}

View File

@ -603,7 +603,7 @@ export enum JobName {
Ocr = 'Ocr',
}
export enum JobCommand {
export enum QueueCommand {
Start = 'start',
Pause = 'pause',
Resume = 'resume',

View File

@ -7,7 +7,6 @@ import { ONE_HOUR } from 'src/constants';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { AuthService } from 'src/services/auth.service';
import { JobService } from 'src/services/job.service';
import { SharedLinkService } from 'src/services/shared-link.service';
import { VersionService } from 'src/services/version.service';
import { OpenGraphTags } from 'src/utils/misc';
@ -40,7 +39,6 @@ const render = (index: string, meta: OpenGraphTags) => {
export class ApiService {
constructor(
private authService: AuthService,
private jobService: JobService,
private sharedLinkService: SharedLinkService,
private versionService: VersionService,
private configRepository: ConfigRepository,

View File

@ -23,6 +23,7 @@ import { NotificationService } from 'src/services/notification.service';
import { OcrService } from 'src/services/ocr.service';
import { PartnerService } from 'src/services/partner.service';
import { PersonService } from 'src/services/person.service';
import { QueueService } from 'src/services/queue.service';
import { SearchService } from 'src/services/search.service';
import { ServerService } from 'src/services/server.service';
import { SessionService } from 'src/services/session.service';
@ -69,6 +70,7 @@ export const services = [
OcrService,
PartnerService,
PersonService,
QueueService,
SearchService,
ServerService,
SessionService,

View File

@ -1,6 +1,4 @@
import { BadRequestException } from '@nestjs/common';
import { defaults, SystemConfig } from 'src/config';
import { ImmichWorker, JobCommand, JobName, JobStatus, QueueName } from 'src/enum';
import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
import { JobService } from 'src/services/job.service';
import { JobItem } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
@ -20,209 +18,6 @@ describe(JobService.name, () => {
expect(sut).toBeDefined();
});
describe('onConfigUpdate', () => {
it('should update concurrency', () => {
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(16);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.StorageTemplateMigration, 1);
});
});
describe('handleNightlyJobs', () => {
it('should run the scheduled jobs', async () => {
await sut.handleNightlyJobs();
expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.AssetDeleteCheck },
{ name: JobName.UserDeleteCheck },
{ name: JobName.PersonCleanup },
{ name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup },
{ name: JobName.MemoryGenerate },
{ name: JobName.UserSyncUsage },
{ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } },
{ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } },
]);
});
});
describe('getAllJobStatus', () => {
it('should get all job statuses', async () => {
mocks.job.getJobCounts.mockResolvedValue({
active: 1,
completed: 1,
failed: 1,
delayed: 1,
waiting: 1,
paused: 1,
});
mocks.job.getQueueStatus.mockResolvedValue({
isActive: true,
isPaused: true,
});
const expectedJobStatus = {
jobCounts: {
active: 1,
completed: 1,
delayed: 1,
failed: 1,
waiting: 1,
paused: 1,
},
queueStatus: {
isActive: true,
isPaused: true,
},
};
await expect(sut.getAllJobsStatus()).resolves.toEqual({
[QueueName.BackgroundTask]: expectedJobStatus,
[QueueName.DuplicateDetection]: expectedJobStatus,
[QueueName.SmartSearch]: expectedJobStatus,
[QueueName.MetadataExtraction]: expectedJobStatus,
[QueueName.Search]: expectedJobStatus,
[QueueName.StorageTemplateMigration]: expectedJobStatus,
[QueueName.Migration]: expectedJobStatus,
[QueueName.ThumbnailGeneration]: expectedJobStatus,
[QueueName.VideoConversion]: expectedJobStatus,
[QueueName.FaceDetection]: expectedJobStatus,
[QueueName.FacialRecognition]: expectedJobStatus,
[QueueName.Sidecar]: expectedJobStatus,
[QueueName.Library]: expectedJobStatus,
[QueueName.Notification]: expectedJobStatus,
[QueueName.BackupDatabase]: expectedJobStatus,
[QueueName.Ocr]: expectedJobStatus,
});
});
});
describe('handleCommand', () => {
it('should handle a pause command', async () => {
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Pause, force: false });
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
});
it('should handle a resume command', async () => {
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Resume, force: false });
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
});
it('should handle an empty command', async () => {
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Empty, force: false });
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
});
it('should not start a job that is already running', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
await expect(
sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
});
it('should handle a start video conversion command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
});
it('should handle a start storage template migration command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.StorageTemplateMigration, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
});
it('should handle a start smart search command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.SmartSearch, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
});
it('should handle a start metadata extraction command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetExtractMetadataQueueAll,
data: { force: false },
});
});
it('should handle a start sidecar command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.Sidecar, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
});
it('should handle a start thumbnail generation command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.ThumbnailGeneration, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetGenerateThumbnailsQueueAll,
data: { force: false },
});
});
it('should handle a start face detection command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.FaceDetection, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
});
it('should handle a start facial recognition command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.FacialRecognition, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
});
it('should handle a start backup database command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.BackupDatabase, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
});
it('should throw a bad request when an invalid queue is used', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await expect(
sut.handleCommand(QueueName.BackgroundTask, { command: JobCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
});
});
describe('onJobRun', () => {
it('should process a successful job', async () => {
mocks.job.run.mockResolvedValue(JobStatus.Success);

View File

@ -1,28 +1,12 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { ClassConstructor } from 'class-transformer';
import { SystemConfig } from 'src/config';
import { OnEvent } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import {
AssetType,
AssetVisibility,
BootstrapEventPriority,
CronJob,
DatabaseLock,
ImmichWorker,
JobCommand,
JobName,
JobStatus,
ManualJobName,
QueueCleanType,
QueueName,
} from 'src/enum';
import { ArgOf, ArgsOf } from 'src/repositories/event.repository';
import { JobCreateDto } from 'src/dtos/job.dto';
import { AssetType, AssetVisibility, JobName, JobStatus, ManualJobName } from 'src/enum';
import { ArgsOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { ConcurrentQueueName, JobItem } from 'src/types';
import { JobItem } from 'src/types';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { handlePromiseError } from 'src/utils/misc';
const asJobItem = (dto: JobCreateDto): JobItem => {
switch (dto.name) {
@ -56,196 +40,12 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
}
};
const asNightlyTasksCron = (config: SystemConfig) => {
const [hours, minutes] = config.nightlyTasks.startTime.split(':').map(Number);
return `${minutes} ${hours} * * *`;
};
@Injectable()
export class JobService extends BaseService {
private services: ClassConstructor<unknown>[] = [];
private nightlyJobsLock = false;
@OnEvent({ name: 'ConfigInit' })
async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) {
if (this.worker === ImmichWorker.Microservices) {
this.updateQueueConcurrency(config);
return;
}
this.nightlyJobsLock = await this.databaseRepository.tryLock(DatabaseLock.NightlyJobs);
if (this.nightlyJobsLock) {
const cronExpression = asNightlyTasksCron(config);
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
this.cronRepository.create({
name: CronJob.NightlyJobs,
expression: cronExpression,
start: true,
onTick: () => handlePromiseError(this.handleNightlyJobs(), this.logger),
});
}
}
@OnEvent({ name: 'ConfigUpdate', server: true })
onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) {
if (this.worker === ImmichWorker.Microservices) {
this.updateQueueConcurrency(config);
return;
}
if (this.nightlyJobsLock) {
const cronExpression = asNightlyTasksCron(config);
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
this.cronRepository.update({ name: CronJob.NightlyJobs, expression: cronExpression, start: true });
}
}
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService })
onBootstrap() {
this.jobRepository.setup(this.services);
if (this.worker === ImmichWorker.Microservices) {
this.jobRepository.startWorkers();
}
}
private updateQueueConcurrency(config: SystemConfig) {
this.logger.debug(`Updating queue concurrency settings`);
for (const queueName of Object.values(QueueName)) {
let concurrency = 1;
if (this.isConcurrentQueue(queueName)) {
concurrency = config.job[queueName].concurrency;
}
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
this.jobRepository.setConcurrency(queueName, concurrency);
}
}
setServices(services: ClassConstructor<unknown>[]) {
this.services = services;
}
async create(dto: JobCreateDto): Promise<void> {
await this.jobRepository.queue(asJobItem(dto));
}
async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise<JobStatusDto> {
this.logger.debug(`Handling command: queue=${queueName},command=${dto.command},force=${dto.force}`);
switch (dto.command) {
case JobCommand.Start: {
await this.start(queueName, dto);
break;
}
case JobCommand.Pause: {
await this.jobRepository.pause(queueName);
break;
}
case JobCommand.Resume: {
await this.jobRepository.resume(queueName);
break;
}
case JobCommand.Empty: {
await this.jobRepository.empty(queueName);
break;
}
case JobCommand.ClearFailed: {
const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.Failed);
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
break;
}
}
return this.getJobStatus(queueName);
}
async getJobStatus(queueName: QueueName): Promise<JobStatusDto> {
const [jobCounts, queueStatus] = await Promise.all([
this.jobRepository.getJobCounts(queueName),
this.jobRepository.getQueueStatus(queueName),
]);
return { jobCounts, queueStatus };
}
async getAllJobsStatus(): Promise<AllJobStatusResponseDto> {
const response = new AllJobStatusResponseDto();
for (const queueName of Object.values(QueueName)) {
response[queueName] = await this.getJobStatus(queueName);
}
return response;
}
private async start(name: QueueName, { force }: JobCommandDto): Promise<void> {
const { isActive } = await this.jobRepository.getQueueStatus(name);
if (isActive) {
throw new BadRequestException(`Job is already running`);
}
await this.eventRepository.emit('QueueStart', { name });
switch (name) {
case QueueName.VideoConversion: {
return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } });
}
case QueueName.StorageTemplateMigration: {
return this.jobRepository.queue({ name: JobName.StorageTemplateMigration });
}
case QueueName.Migration: {
return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll });
}
case QueueName.SmartSearch: {
return this.jobRepository.queue({ name: JobName.SmartSearchQueueAll, data: { force } });
}
case QueueName.DuplicateDetection: {
return this.jobRepository.queue({ name: JobName.AssetDetectDuplicatesQueueAll, data: { force } });
}
case QueueName.MetadataExtraction: {
return this.jobRepository.queue({ name: JobName.AssetExtractMetadataQueueAll, data: { force } });
}
case QueueName.Sidecar: {
return this.jobRepository.queue({ name: JobName.SidecarQueueAll, data: { force } });
}
case QueueName.ThumbnailGeneration: {
return this.jobRepository.queue({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force } });
}
case QueueName.FaceDetection: {
return this.jobRepository.queue({ name: JobName.AssetDetectFacesQueueAll, data: { force } });
}
case QueueName.FacialRecognition: {
return this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force } });
}
case QueueName.Library: {
return this.jobRepository.queue({ name: JobName.LibraryScanQueueAll, data: { force } });
}
case QueueName.BackupDatabase: {
return this.jobRepository.queue({ name: JobName.DatabaseBackup, data: { force } });
}
case QueueName.Ocr: {
return this.jobRepository.queue({ name: JobName.OcrQueueAll, data: { force } });
}
default: {
throw new BadRequestException(`Invalid job name: ${name}`);
}
}
}
@OnEvent({ name: 'JobRun' })
async onJobRun(...[queueName, job]: ArgsOf<'JobRun'>) {
try {
@ -262,50 +62,6 @@ export class JobService extends BaseService {
}
}
private isConcurrentQueue(name: QueueName): name is ConcurrentQueueName {
return ![
QueueName.FacialRecognition,
QueueName.StorageTemplateMigration,
QueueName.DuplicateDetection,
QueueName.BackupDatabase,
].includes(name);
}
async handleNightlyJobs() {
const config = await this.getConfig({ withCache: false });
const jobs: JobItem[] = [];
if (config.nightlyTasks.databaseCleanup) {
jobs.push(
{ name: JobName.AssetDeleteCheck },
{ name: JobName.UserDeleteCheck },
{ name: JobName.PersonCleanup },
{ name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup },
);
}
if (config.nightlyTasks.generateMemories) {
jobs.push({ name: JobName.MemoryGenerate });
}
if (config.nightlyTasks.syncQuotaUsage) {
jobs.push({ name: JobName.UserSyncUsage });
}
if (config.nightlyTasks.missingThumbnails) {
jobs.push({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } });
}
if (config.nightlyTasks.clusterNewFaces) {
jobs.push({ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } });
}
await this.jobRepository.queueAll(jobs);
}
/**
* Queue follow up jobs
*/

View File

@ -0,0 +1,223 @@
import { BadRequestException } from '@nestjs/common';
import { defaults, SystemConfig } from 'src/config';
import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum';
import { QueueService } from 'src/services/queue.service';
import { newTestService, ServiceMocks } from 'test/utils';
describe(QueueService.name, () => {
let sut: QueueService;
let mocks: ServiceMocks;
beforeEach(() => {
({ sut, mocks } = newTestService(QueueService));
mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices);
});
it('should work', () => {
expect(sut).toBeDefined();
});
describe('onConfigUpdate', () => {
it('should update concurrency', () => {
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(16);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.StorageTemplateMigration, 1);
});
});
describe('handleNightlyJobs', () => {
it('should run the scheduled jobs', async () => {
await sut.handleNightlyJobs();
expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.AssetDeleteCheck },
{ name: JobName.UserDeleteCheck },
{ name: JobName.PersonCleanup },
{ name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup },
{ name: JobName.MemoryGenerate },
{ name: JobName.UserSyncUsage },
{ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } },
{ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } },
]);
});
});
describe('getAllJobStatus', () => {
it('should get all job statuses', async () => {
mocks.job.getJobCounts.mockResolvedValue({
active: 1,
completed: 1,
failed: 1,
delayed: 1,
waiting: 1,
paused: 1,
});
mocks.job.getQueueStatus.mockResolvedValue({
isActive: true,
isPaused: true,
});
const expectedJobStatus = {
jobCounts: {
active: 1,
completed: 1,
delayed: 1,
failed: 1,
waiting: 1,
paused: 1,
},
queueStatus: {
isActive: true,
isPaused: true,
},
};
await expect(sut.getAll()).resolves.toEqual({
[QueueName.BackgroundTask]: expectedJobStatus,
[QueueName.DuplicateDetection]: expectedJobStatus,
[QueueName.SmartSearch]: expectedJobStatus,
[QueueName.MetadataExtraction]: expectedJobStatus,
[QueueName.Search]: expectedJobStatus,
[QueueName.StorageTemplateMigration]: expectedJobStatus,
[QueueName.Migration]: expectedJobStatus,
[QueueName.ThumbnailGeneration]: expectedJobStatus,
[QueueName.VideoConversion]: expectedJobStatus,
[QueueName.FaceDetection]: expectedJobStatus,
[QueueName.FacialRecognition]: expectedJobStatus,
[QueueName.Sidecar]: expectedJobStatus,
[QueueName.Library]: expectedJobStatus,
[QueueName.Notification]: expectedJobStatus,
[QueueName.BackupDatabase]: expectedJobStatus,
[QueueName.Ocr]: expectedJobStatus,
});
});
});
describe('handleCommand', () => {
it('should handle a pause command', async () => {
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false });
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
});
it('should handle a resume command', async () => {
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false });
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
});
it('should handle an empty command', async () => {
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false });
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
});
it('should not start a job that is already running', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
await expect(
sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
});
it('should handle a start video conversion command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } });
});
it('should handle a start storage template migration command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
});
it('should handle a start smart search command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.SmartSearch, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } });
});
it('should handle a start metadata extraction command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetExtractMetadataQueueAll,
data: { force: false },
});
});
it('should handle a start sidecar command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.Sidecar, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } });
});
it('should handle a start thumbnail generation command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.AssetGenerateThumbnailsQueueAll,
data: { force: false },
});
});
it('should handle a start face detection command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.FaceDetection, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } });
});
it('should handle a start facial recognition command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } });
});
it('should handle a start backup database command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.runCommand(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } });
});
it('should throw a bad request when an invalid queue is used', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await expect(
sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,250 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { ClassConstructor } from 'class-transformer';
import { SystemConfig } from 'src/config';
import { OnEvent } from 'src/decorators';
import { QueueCommandDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto';
import {
BootstrapEventPriority,
CronJob,
DatabaseLock,
ImmichWorker,
JobName,
QueueCleanType,
QueueCommand,
QueueName,
} from 'src/enum';
import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { ConcurrentQueueName, JobItem } from 'src/types';
import { handlePromiseError } from 'src/utils/misc';
const asNightlyTasksCron = (config: SystemConfig) => {
const [hours, minutes] = config.nightlyTasks.startTime.split(':').map(Number);
return `${minutes} ${hours} * * *`;
};
@Injectable()
export class QueueService extends BaseService {
private services: ClassConstructor<unknown>[] = [];
private nightlyJobsLock = false;
@OnEvent({ name: 'ConfigInit' })
async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) {
if (this.worker === ImmichWorker.Microservices) {
this.updateConcurrency(config);
return;
}
this.nightlyJobsLock = await this.databaseRepository.tryLock(DatabaseLock.NightlyJobs);
if (this.nightlyJobsLock) {
const cronExpression = asNightlyTasksCron(config);
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
this.cronRepository.create({
name: CronJob.NightlyJobs,
expression: cronExpression,
start: true,
onTick: () => handlePromiseError(this.handleNightlyJobs(), this.logger),
});
}
}
@OnEvent({ name: 'ConfigUpdate', server: true })
onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) {
if (this.worker === ImmichWorker.Microservices) {
this.updateConcurrency(config);
return;
}
if (this.nightlyJobsLock) {
const cronExpression = asNightlyTasksCron(config);
this.logger.debug(`Scheduling nightly jobs for ${cronExpression}`);
this.cronRepository.update({ name: CronJob.NightlyJobs, expression: cronExpression, start: true });
}
}
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService })
onBootstrap() {
this.jobRepository.setup(this.services);
if (this.worker === ImmichWorker.Microservices) {
this.jobRepository.startWorkers();
}
}
private updateConcurrency(config: SystemConfig) {
this.logger.debug(`Updating queue concurrency settings`);
for (const queueName of Object.values(QueueName)) {
let concurrency = 1;
if (this.isConcurrentQueue(queueName)) {
concurrency = config.job[queueName].concurrency;
}
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
this.jobRepository.setConcurrency(queueName, concurrency);
}
}
setServices(services: ClassConstructor<unknown>[]) {
this.services = services;
}
async runCommand(name: QueueName, dto: QueueCommandDto): Promise<QueueResponseDto> {
this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`);
switch (dto.command) {
case QueueCommand.Start: {
await this.start(name, dto);
break;
}
case QueueCommand.Pause: {
await this.jobRepository.pause(name);
break;
}
case QueueCommand.Resume: {
await this.jobRepository.resume(name);
break;
}
case QueueCommand.Empty: {
await this.jobRepository.empty(name);
break;
}
case QueueCommand.ClearFailed: {
const failedJobs = await this.jobRepository.clear(name, QueueCleanType.Failed);
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
break;
}
}
return this.getByName(name);
}
async getAll(): Promise<QueuesResponseDto> {
const response = new QueuesResponseDto();
for (const name of Object.values(QueueName)) {
response[name] = await this.getByName(name);
}
return response;
}
async getByName(name: QueueName): Promise<QueueResponseDto> {
const [jobCounts, queueStatus] = await Promise.all([
this.jobRepository.getJobCounts(name),
this.jobRepository.getQueueStatus(name),
]);
return { jobCounts, queueStatus };
}
private async start(name: QueueName, { force }: QueueCommandDto): Promise<void> {
const { isActive } = await this.jobRepository.getQueueStatus(name);
if (isActive) {
throw new BadRequestException(`Job is already running`);
}
await this.eventRepository.emit('QueueStart', { name });
switch (name) {
case QueueName.VideoConversion: {
return this.jobRepository.queue({ name: JobName.AssetEncodeVideoQueueAll, data: { force } });
}
case QueueName.StorageTemplateMigration: {
return this.jobRepository.queue({ name: JobName.StorageTemplateMigration });
}
case QueueName.Migration: {
return this.jobRepository.queue({ name: JobName.FileMigrationQueueAll });
}
case QueueName.SmartSearch: {
return this.jobRepository.queue({ name: JobName.SmartSearchQueueAll, data: { force } });
}
case QueueName.DuplicateDetection: {
return this.jobRepository.queue({ name: JobName.AssetDetectDuplicatesQueueAll, data: { force } });
}
case QueueName.MetadataExtraction: {
return this.jobRepository.queue({ name: JobName.AssetExtractMetadataQueueAll, data: { force } });
}
case QueueName.Sidecar: {
return this.jobRepository.queue({ name: JobName.SidecarQueueAll, data: { force } });
}
case QueueName.ThumbnailGeneration: {
return this.jobRepository.queue({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force } });
}
case QueueName.FaceDetection: {
return this.jobRepository.queue({ name: JobName.AssetDetectFacesQueueAll, data: { force } });
}
case QueueName.FacialRecognition: {
return this.jobRepository.queue({ name: JobName.FacialRecognitionQueueAll, data: { force } });
}
case QueueName.Library: {
return this.jobRepository.queue({ name: JobName.LibraryScanQueueAll, data: { force } });
}
case QueueName.BackupDatabase: {
return this.jobRepository.queue({ name: JobName.DatabaseBackup, data: { force } });
}
case QueueName.Ocr: {
return this.jobRepository.queue({ name: JobName.OcrQueueAll, data: { force } });
}
default: {
throw new BadRequestException(`Invalid job name: ${name}`);
}
}
}
private isConcurrentQueue(name: QueueName): name is ConcurrentQueueName {
return ![
QueueName.FacialRecognition,
QueueName.StorageTemplateMigration,
QueueName.DuplicateDetection,
QueueName.BackupDatabase,
].includes(name);
}
async handleNightlyJobs() {
const config = await this.getConfig({ withCache: false });
const jobs: JobItem[] = [];
if (config.nightlyTasks.databaseCleanup) {
jobs.push(
{ name: JobName.AssetDeleteCheck },
{ name: JobName.UserDeleteCheck },
{ name: JobName.PersonCleanup },
{ name: JobName.MemoryCleanup },
{ name: JobName.SessionCleanup },
{ name: JobName.AuditTableCleanup },
{ name: JobName.AuditLogCleanup },
);
}
if (config.nightlyTasks.generateMemories) {
jobs.push({ name: JobName.MemoryGenerate });
}
if (config.nightlyTasks.syncQuotaUsage) {
jobs.push({ name: JobName.UserSyncUsage });
}
if (config.nightlyTasks.missingThumbnails) {
jobs.push({ name: JobName.AssetGenerateThumbnailsQueueAll, data: { force: false } });
}
if (config.nightlyTasks.clusterNewFaces) {
jobs.push({ name: JobName.FacialRecognitionQueueAll, data: { force: false, nightly: true } });
}
await this.jobRepository.queueAll(jobs);
}
}

View File

@ -4,8 +4,8 @@
import { SettingInputFieldType } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
import { getJobName } from '$lib/utils';
import { JobName, type SystemConfigJobDto } from '@immich/sdk';
import { getQueueName } from '$lib/utils';
import { QueueName, type SystemConfigJobDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@ -13,18 +13,18 @@
const config = $derived(systemConfigManager.value);
let configToEdit = $state(systemConfigManager.cloneValue());
const jobNames = [
JobName.ThumbnailGeneration,
JobName.MetadataExtraction,
JobName.Library,
JobName.Sidecar,
JobName.SmartSearch,
JobName.FaceDetection,
JobName.FacialRecognition,
JobName.VideoConversion,
JobName.StorageTemplateMigration,
JobName.Migration,
JobName.Ocr,
const queueNames = [
QueueName.ThumbnailGeneration,
QueueName.MetadataExtraction,
QueueName.Library,
QueueName.Sidecar,
QueueName.SmartSearch,
QueueName.FaceDetection,
QueueName.FacialRecognition,
QueueName.VideoConversion,
QueueName.StorageTemplateMigration,
QueueName.Migration,
QueueName.Ocr,
];
function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto {
@ -35,22 +35,22 @@
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
{#each jobNames as jobName (jobName)}
{#each queueNames as queueName (queueName)}
<div class="ms-4 mt-4 flex flex-col gap-4">
{#if isSystemConfigJobDto(jobName)}
{#if isSystemConfigJobDto(queueName)}
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
{disabled}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
description=""
bind:value={configToEdit.job[jobName].concurrency}
bind:value={configToEdit.job[queueName].concurrency}
required={true}
isEdited={!(configToEdit.job[jobName].concurrency == config.job[jobName].concurrency)}
isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)}
/>
{:else}
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
description=""
value={1}
disabled={true}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import Badge from '$lib/elements/Badge.svelte';
import { locale } from '$lib/stores/preferences.store';
import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusDto } from '@immich/sdk';
import { Icon, IconButton } from '@immich/ui';
import {
mdiAlertCircle,
@ -22,21 +22,21 @@
title: string;
subtitle: string | undefined;
description: Component | undefined;
jobCounts: JobCountsDto;
statistics: QueueStatisticsDto;
queueStatus: QueueStatusDto;
icon: string;
disabled?: boolean;
allText: string | undefined;
refreshText: string | undefined;
missingText: string;
onCommand: (command: JobCommandDto) => void;
onCommand: (command: QueueCommandDto) => void;
}
let {
title,
subtitle,
description,
jobCounts,
statistics,
queueStatus,
icon,
disabled = false,
@ -46,7 +46,7 @@
onCommand,
}: Props = $props();
let waitingCount = $derived(jobCounts.waiting + jobCounts.paused + jobCounts.delayed);
let waitingCount = $derived(statistics.waiting + statistics.paused + statistics.delayed);
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
let multipleButtons = $derived(allText || refreshText);
@ -67,11 +67,11 @@
<span class="uppercase">{title}</span>
</span>
<div class="flex gap-2">
{#if jobCounts.failed > 0}
{#if statistics.failed > 0}
<Badge>
<div class="flex flex-row gap-1">
<span class="text-sm">
{$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })}
{$t('admin.jobs_failed', { values: { jobCount: statistics.failed.toLocaleString($locale) } })}
</span>
<IconButton
color="primary"
@ -79,15 +79,15 @@
aria-label={$t('clear_message')}
size="tiny"
shape="round"
onclick={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
onclick={() => onCommand({ command: QueueCommand.ClearFailed, force: false })}
/>
</div>
</Badge>
{/if}
{#if jobCounts.delayed > 0}
{#if statistics.delayed > 0}
<Badge>
<span class="text-sm">
{$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })}
{$t('admin.jobs_delayed', { values: { jobCount: statistics.delayed.toLocaleString($locale) } })}
</span>
</Badge>
{/if}
@ -111,7 +111,7 @@
>
<p>{$t('active')}</p>
<p class="text-2xl">
{jobCounts.active.toLocaleString($locale)}
{statistics.active.toLocaleString($locale)}
</p>
</div>
@ -131,7 +131,7 @@
<JobTileButton
disabled={true}
color="light-gray"
onClick={() => onCommand({ command: JobCommand.Start, force: false })}
onClick={() => onCommand({ command: QueueCommand.Start, force: false })}
>
<Icon icon={mdiAlertCircle} size="36" />
<span class="uppercase">{$t('disabled')}</span>
@ -140,20 +140,20 @@
{#if !disabled && !isIdle}
{#if waitingCount > 0}
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Empty, force: false })}>
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}>
<Icon icon={mdiClose} size="24" />
<span class="uppercase">{$t('clear')}</span>
</JobTileButton>
{/if}
{#if queueStatus.isPaused}
{@const size = waitingCount > 0 ? '24' : '48'}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Resume, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
<!-- size property is not reactive, so have to use width and height -->
<Icon icon={mdiFastForward} {size} />
<span class="uppercase">{$t('resume')}</span>
</JobTileButton>
{:else}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Pause, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}>
<Icon icon={mdiPause} size="24" />
<span class="uppercase">{$t('pause')}</span>
</JobTileButton>
@ -162,25 +162,25 @@
{#if !disabled && multipleButtons && isIdle}
{#if allText}
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: JobCommand.Start, force: true })}>
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}>
<Icon icon={mdiAllInclusive} size="24" />
<span class="uppercase">{allText}</span>
</JobTileButton>
{/if}
{#if refreshText}
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Start, force: undefined })}>
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}>
<Icon icon={mdiImageRefreshOutline} size="24" />
<span class="uppercase">{refreshText}</span>
</JobTileButton>
{/if}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
<Icon icon={mdiSelectionSearch} size="24" />
<span class="uppercase">{missingText}</span>
</JobTileButton>
{/if}
{#if !disabled && !multipleButtons && isIdle}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
<Icon icon={mdiPlay} size="48" />
<span class="uppercase">{missingText}</span>
</JobTileButton>

View File

@ -1,8 +1,14 @@
<script lang="ts">
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { getJobName } from '$lib/utils';
import { getQueueName } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { JobCommand, JobName, sendJobCommand, type AllJobStatusResponseDto, type JobCommandDto } from '@immich/sdk';
import {
QueueCommand,
type QueueCommandDto,
QueueName,
type QueuesResponseDto,
runQueueCommandLegacy,
} from '@immich/sdk';
import { modalManager, toastManager } from '@immich/ui';
import {
mdiContentDuplicate,
@ -23,7 +29,7 @@
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
interface Props {
jobs: AllJobStatusResponseDto;
jobs: QueuesResponseDto;
}
let { jobs = $bindable() }: Props = $props();
@ -38,17 +44,17 @@
missingText: string;
disabled?: boolean;
icon: string;
handleCommand?: (jobId: JobName, jobCommand: JobCommandDto) => Promise<void>;
handleCommand?: (jobId: QueueName, jobCommand: QueueCommandDto) => Promise<void>;
};
const handleConfirmCommand = async (jobId: JobName, dto: JobCommandDto) => {
const handleConfirmCommand = async (jobId: QueueName, dto: QueueCommandDto) => {
if (dto.force) {
const isConfirmed = await modalManager.showDialog({
prompt: $t('admin.confirm_reprocess_all_faces'),
});
if (isConfirmed) {
await handleCommand(jobId, { command: JobCommand.Start, force: true });
await handleCommand(jobId, { command: QueueCommand.Start, force: true });
return;
}
@ -58,54 +64,54 @@
await handleCommand(jobId, dto);
};
let jobDetails: Partial<Record<JobName, JobDetails>> = {
[JobName.ThumbnailGeneration]: {
let jobDetails: Partial<Record<QueueName, JobDetails>> = {
[QueueName.ThumbnailGeneration]: {
icon: mdiFileJpgBox,
title: $getJobName(JobName.ThumbnailGeneration),
title: $getQueueName(QueueName.ThumbnailGeneration),
subtitle: $t('admin.thumbnail_generation_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.MetadataExtraction]: {
[QueueName.MetadataExtraction]: {
icon: mdiTable,
title: $getJobName(JobName.MetadataExtraction),
title: $getQueueName(QueueName.MetadataExtraction),
subtitle: $t('admin.metadata_extraction_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.Library]: {
[QueueName.Library]: {
icon: mdiLibraryShelves,
title: $getJobName(JobName.Library),
title: $getQueueName(QueueName.Library),
subtitle: $t('admin.library_tasks_description'),
missingText: $t('rescan'),
},
[JobName.Sidecar]: {
title: $getJobName(JobName.Sidecar),
[QueueName.Sidecar]: {
title: $getQueueName(QueueName.Sidecar),
icon: mdiFileXmlBox,
subtitle: $t('admin.sidecar_job_description'),
allText: $t('sync'),
missingText: $t('discover'),
disabled: !featureFlags.sidecar,
},
[JobName.SmartSearch]: {
[QueueName.SmartSearch]: {
icon: mdiImageSearch,
title: $getJobName(JobName.SmartSearch),
title: $getQueueName(QueueName.SmartSearch),
subtitle: $t('admin.smart_search_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.smartSearch,
},
[JobName.DuplicateDetection]: {
[QueueName.DuplicateDetection]: {
icon: mdiContentDuplicate,
title: $getJobName(JobName.DuplicateDetection),
title: $getQueueName(QueueName.DuplicateDetection),
subtitle: $t('admin.duplicate_detection_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.duplicateDetection,
},
[JobName.FaceDetection]: {
[QueueName.FaceDetection]: {
icon: mdiFaceRecognition,
title: $getJobName(JobName.FaceDetection),
title: $getQueueName(QueueName.FaceDetection),
subtitle: $t('admin.face_detection_description'),
allText: $t('reset'),
refreshText: $t('refresh'),
@ -113,67 +119,67 @@
handleCommand: handleConfirmCommand,
disabled: !featureFlags.facialRecognition,
},
[JobName.FacialRecognition]: {
[QueueName.FacialRecognition]: {
icon: mdiTagFaces,
title: $getJobName(JobName.FacialRecognition),
title: $getQueueName(QueueName.FacialRecognition),
subtitle: $t('admin.facial_recognition_job_description'),
allText: $t('reset'),
missingText: $t('missing'),
handleCommand: handleConfirmCommand,
disabled: !featureFlags.facialRecognition,
},
[JobName.Ocr]: {
[QueueName.Ocr]: {
icon: mdiOcr,
title: $getJobName(JobName.Ocr),
title: $getQueueName(QueueName.Ocr),
subtitle: $t('admin.ocr_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.ocr,
},
[JobName.VideoConversion]: {
[QueueName.VideoConversion]: {
icon: mdiVideo,
title: $getJobName(JobName.VideoConversion),
title: $getQueueName(QueueName.VideoConversion),
subtitle: $t('admin.video_conversion_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.StorageTemplateMigration]: {
[QueueName.StorageTemplateMigration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.StorageTemplateMigration),
title: $getQueueName(QueueName.StorageTemplateMigration),
missingText: $t('start'),
description: StorageMigrationDescription,
},
[JobName.Migration]: {
[QueueName.Migration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.Migration),
title: $getQueueName(QueueName.Migration),
subtitle: $t('admin.migration_job_description'),
missingText: $t('start'),
},
};
let jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
let jobList = Object.entries(jobDetails) as [QueueName, JobDetails][];
async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) {
const title = jobDetails[jobId]?.title;
async function handleCommand(name: QueueName, dto: QueueCommandDto) {
const title = jobDetails[name]?.title;
try {
jobs[jobId] = await sendJobCommand({ id: jobId, jobCommandDto: jobCommand });
jobs[name] = await runQueueCommandLegacy({ name, queueCommandDto: dto });
switch (jobCommand.command) {
case JobCommand.Empty: {
switch (dto.command) {
case QueueCommand.Empty: {
toastManager.success($t('admin.cleared_jobs', { values: { job: title } }));
break;
}
}
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: jobCommand.command, job: title } }));
handleError(error, $t('admin.failed_job_command', { values: { command: dto.command, job: title } }));
}
}
</script>
<div class="flex flex-col gap-7">
{#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }] (jobName)}
{@const { jobCounts, queueStatus } = jobs[jobName]}
{@const { jobCounts: statistics, queueStatus } = jobs[jobName]}
<JobTile
{icon}
{title}
@ -183,7 +189,7 @@
{allText}
{refreshText}
{missingText}
{jobCounts}
{statistics}
{queueStatus}
onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}
/>

View File

@ -5,8 +5,8 @@ import { handleError } from '$lib/utils/handle-error';
import {
AssetJobName,
AssetMediaSize,
JobName,
MemoryType,
QueueName,
finishOAuth,
getAssetOriginalPath,
getAssetPlaybackPath,
@ -143,28 +143,28 @@ export const downloadRequest = <TBody = unknown>(options: DownloadRequestOptions
});
};
export const getJobName = derived(t, ($t) => {
return (jobName: JobName) => {
const names: Record<JobName, string> = {
[JobName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'),
[JobName.MetadataExtraction]: $t('admin.metadata_extraction_job'),
[JobName.Sidecar]: $t('admin.sidecar_job'),
[JobName.SmartSearch]: $t('admin.machine_learning_smart_search'),
[JobName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'),
[JobName.FaceDetection]: $t('admin.face_detection'),
[JobName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'),
[JobName.VideoConversion]: $t('admin.video_conversion_job'),
[JobName.StorageTemplateMigration]: $t('admin.storage_template_migration'),
[JobName.Migration]: $t('admin.migration_job'),
[JobName.BackgroundTask]: $t('admin.background_task_job'),
[JobName.Search]: $t('search'),
[JobName.Library]: $t('external_libraries'),
[JobName.Notifications]: $t('notifications'),
[JobName.BackupDatabase]: $t('admin.backup_database'),
[JobName.Ocr]: $t('admin.machine_learning_ocr'),
export const getQueueName = derived(t, ($t) => {
return (name: QueueName) => {
const names: Record<QueueName, string> = {
[QueueName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'),
[QueueName.MetadataExtraction]: $t('admin.metadata_extraction_job'),
[QueueName.Sidecar]: $t('admin.sidecar_job'),
[QueueName.SmartSearch]: $t('admin.machine_learning_smart_search'),
[QueueName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'),
[QueueName.FaceDetection]: $t('admin.face_detection'),
[QueueName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'),
[QueueName.VideoConversion]: $t('admin.video_conversion_job'),
[QueueName.StorageTemplateMigration]: $t('admin.storage_template_migration'),
[QueueName.Migration]: $t('admin.migration_job'),
[QueueName.BackgroundTask]: $t('admin.background_task_job'),
[QueueName.Search]: $t('search'),
[QueueName.Library]: $t('external_libraries'),
[QueueName.Notifications]: $t('notifications'),
[QueueName.BackupDatabase]: $t('admin.backup_database'),
[QueueName.Ocr]: $t('admin.machine_learning_ocr'),
};
return names[jobName];
return names[name];
};
});

View File

@ -5,13 +5,7 @@
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
import { asyncTimeout } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
getAllJobsStatus,
JobCommand,
sendJobCommand,
type AllJobStatusResponseDto,
type JobName,
} from '@immich/sdk';
import { getQueuesLegacy, QueueCommand, QueueName, runQueueCommandLegacy, type QueuesResponseDto } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui';
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
@ -24,23 +18,23 @@
let { data }: Props = $props();
let jobs: AllJobStatusResponseDto | undefined = $state();
let jobs: QueuesResponseDto | undefined = $state();
let running = true;
const pausedJobs = $derived(
Object.entries(jobs ?? {})
.filter(([_, jobStatus]) => jobStatus.queueStatus?.isPaused)
.map(([jobName]) => jobName as JobName),
.filter(([_, queue]) => queue.queueStatus?.isPaused)
.map(([name]) => name as QueueName),
);
const handleResumePausedJobs = async () => {
try {
for (const jobName of pausedJobs) {
await sendJobCommand({ id: jobName, jobCommandDto: { command: JobCommand.Resume, force: false } });
for (const name of pausedJobs) {
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
}
// Refresh jobs status immediately after resuming
jobs = await getAllJobsStatus();
jobs = await getQueuesLegacy();
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
}
@ -48,7 +42,7 @@
onMount(async () => {
while (running) {
jobs = await getAllJobsStatus();
jobs = await getQueuesLegacy();
await asyncTimeout(5000);
}
});

View File

@ -1,12 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAllJobsStatus } from '@immich/sdk';
import { getQueuesLegacy } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const jobs = await getAllJobsStatus();
const jobs = await getQueuesLegacy();
const $t = await getFormatter();
return {

View File

@ -17,10 +17,10 @@
getAllLibraries,
getLibraryStatistics,
getUserAdmin,
JobCommand,
JobName,
QueueCommand,
QueueName,
runQueueCommandLegacy,
scanLibrary,
sendJobCommand,
updateLibrary,
type LibraryResponseDto,
type LibraryStatsResponseDto,
@ -151,7 +151,7 @@
const handleScanAll = async () => {
try {
await sendJobCommand({ id: JobName.Library, jobCommandDto: { command: JobCommand.Start } });
await runQueueCommandLegacy({ name: QueueName.Library, queueCommandDto: { command: QueueCommand.Start } });
toastManager.info($t('admin.refreshing_all_libraries'));
} catch (error) {