Files
immich/web/src/lib/services/queue.service.ts
2025-12-04 11:09:38 -05:00

269 lines
8.0 KiB
TypeScript

import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { queueManager } from '$lib/managers/queue-manager.svelte';
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
import type { HeaderButtonActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
emptyQueue,
getQueue,
QueueCommand,
QueueName,
runQueueCommandLegacy,
updateQueue,
type QueueResponseDto,
} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem, type IconLike } from '@immich/ui';
import {
mdiClose,
mdiCog,
mdiContentDuplicate,
mdiDatabaseOutline,
mdiFaceRecognition,
mdiFileJpgBox,
mdiFileXmlBox,
mdiFolderMove,
mdiImageSearch,
mdiLibraryShelves,
mdiOcr,
mdiPause,
mdiPlay,
mdiPlus,
mdiStateMachine,
mdiTable,
mdiTagFaces,
mdiTrashCanOutline,
mdiTrayFull,
mdiVideo,
} from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
type QueueItem = {
icon: IconLike;
title: string;
subtitle?: string;
};
export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[] | undefined) => {
const pausedQueues = (queues ?? []).filter(({ isPaused }) => isPaused).map(({ name }) => name);
const ResumePaused: HeaderButtonActionItem = {
title: $t('resume_paused_jobs', { values: { count: pausedQueues.length } }),
$if: () => pausedQueues.length > 0,
icon: mdiPlay,
onAction: () => handleResumePausedJobs(pausedQueues),
data: {
title: pausedQueues.join(', '),
},
};
const CreateJob: ActionItem = {
icon: mdiPlus,
title: $t('admin.create_job'),
type: $t('command'),
shortcuts: { shift: true, key: 'n' },
onAction: async () => {
await modalManager.show(JobCreateModal, {});
},
};
const ManageConcurrency: ActionItem = {
icon: mdiCog,
title: $t('admin.manage_concurrency'),
description: $t('admin.manage_concurrency_description'),
type: $t('page'),
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
};
return { ResumePaused, ManageConcurrency, CreateJob };
};
export const getQueueActions = ($t: MessageFormatter, queue: QueueResponseDto) => {
const Pause: ActionItem = {
icon: mdiPause,
title: $t('pause'),
$if: () => !queue.isPaused,
onAction: () => handlePauseQueue(queue),
};
const Resume: ActionItem = {
icon: mdiPlay,
title: $t('resume'),
$if: () => queue.isPaused,
onAction: () => handleResumeQueue(queue),
};
const Empty: ActionItem = {
icon: mdiClose,
title: $t('clear'),
onAction: () => handleEmptyQueue(queue),
};
const RemoveFailedJobs: ActionItem = {
icon: mdiTrashCanOutline,
color: 'danger',
title: $t('admin.remove_failed_jobs'),
onAction: () => handleRemoveFailedJobs(queue),
};
return { Pause, Resume, Empty, RemoveFailedJobs };
};
export const handlePauseQueue = async (queue: QueueResponseDto) => {
const response = await updateQueue({ name: queue.name, queueUpdateDto: { isPaused: true } });
eventManager.emit('QueueUpdate', response);
};
export const handleResumeQueue = async (queue: QueueResponseDto) => {
const response = await updateQueue({ name: queue.name, queueUpdateDto: { isPaused: false } });
eventManager.emit('QueueUpdate', response);
};
export const handleEmptyQueue = async (queue: QueueResponseDto) => {
const $t = await getFormatter();
const item = asQueueItem($t, queue);
try {
await emptyQueue({ name: queue.name, queueDeleteDto: { failed: false } });
const response = await getQueue({ name: queue.name });
eventManager.emit('QueueUpdate', response);
toastManager.success($t('admin.cleared_jobs', { values: { job: item.title } }));
} catch (error) {
handleError(error, $t('errors.something_went_wrong'));
}
};
const handleResumePausedJobs = async (queues: QueueName[]) => {
const $t = await getFormatter();
try {
for (const name of queues) {
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
}
await queueManager.refresh();
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
}
};
const handleRemoveFailedJobs = async (queue: QueueResponseDto) => {
const $t = await getFormatter();
try {
await emptyQueue({ name: queue.name, queueDeleteDto: { failed: true } });
const response = await getQueue({ name: queue.name });
eventManager.emit('QueueUpdate', response);
toastManager.success();
} catch (error) {
handleError(error, $t('errors.something_went_wrong'));
}
};
export const asQueueItem = ($t: MessageFormatter, queue: { name: QueueName }): QueueItem => {
// TODO merge this mapping with data from QueuePanel.svelte
const items: Record<QueueName, QueueItem> = {
[QueueName.ThumbnailGeneration]: {
icon: mdiFileJpgBox,
title: $t('admin.thumbnail_generation_job'),
subtitle: $t('admin.thumbnail_generation_job_description'),
},
[QueueName.MetadataExtraction]: {
icon: mdiTable,
title: $t('admin.metadata_extraction_job'),
subtitle: $t('admin.metadata_extraction_job_description'),
},
[QueueName.Library]: {
icon: mdiLibraryShelves,
title: $t('external_libraries'),
subtitle: $t('admin.library_tasks_description'),
},
[QueueName.Sidecar]: {
title: $t('admin.sidecar_job'),
icon: mdiFileXmlBox,
subtitle: $t('admin.sidecar_job_description'),
},
[QueueName.SmartSearch]: {
icon: mdiImageSearch,
title: $t('admin.machine_learning_smart_search'),
subtitle: $t('admin.smart_search_job_description'),
},
[QueueName.DuplicateDetection]: {
icon: mdiContentDuplicate,
title: $t('admin.machine_learning_duplicate_detection'),
subtitle: $t('admin.duplicate_detection_job_description'),
},
[QueueName.FaceDetection]: {
icon: mdiFaceRecognition,
title: $t('admin.face_detection'),
subtitle: $t('admin.face_detection_description'),
},
[QueueName.FacialRecognition]: {
icon: mdiTagFaces,
title: $t('admin.machine_learning_facial_recognition'),
subtitle: $t('admin.facial_recognition_job_description'),
},
[QueueName.Ocr]: {
icon: mdiOcr,
title: $t('admin.machine_learning_ocr'),
subtitle: $t('admin.ocr_job_description'),
},
[QueueName.VideoConversion]: {
icon: mdiVideo,
title: $t('admin.video_conversion_job'),
subtitle: $t('admin.video_conversion_job_description'),
},
[QueueName.StorageTemplateMigration]: {
icon: mdiFolderMove,
title: $t('admin.storage_template_migration'),
},
[QueueName.Migration]: {
icon: mdiFolderMove,
title: $t('admin.migration_job'),
subtitle: $t('admin.migration_job_description'),
},
[QueueName.BackgroundTask]: {
icon: mdiTrayFull,
title: $t('admin.background_task_job'),
},
[QueueName.Search]: {
icon: '',
title: $t('search'),
},
[QueueName.Notifications]: {
icon: '',
title: $t('notifications'),
},
[QueueName.BackupDatabase]: {
icon: mdiDatabaseOutline,
title: $t('admin.backup_database'),
},
[QueueName.Workflow]: {
icon: mdiStateMachine,
title: $t('workflow'),
},
};
return items[queue.name];
};
export const asQueueSlug = (name: QueueName) => {
return name.replaceAll(/[A-Z]/g, (m) => '-' + m.toLowerCase());
};
export const fromQueueSlug = (slug: string): QueueName | undefined => {
const name = slug.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
if (Object.values(QueueName).includes(name as QueueName)) {
return name as QueueName;
}
};
export const getQueueDetailUrl = (queue: QueueResponseDto) => {
return `${AppRoute.ADMIN_QUEUES}/${asQueueSlug(queue.name)}`;
};
export const handleViewQueue = (queue: QueueResponseDto) => {
return goto(getQueueDetailUrl(queue));
};