mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
refactor(server): job repository (#1382)
* refactor(server): job repository * refactor: job repository * chore: generate open-api * fix: job panel * Remove incorrect subtitle Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
f4c90426a5
commit
4cfac47674
34 changed files with 418 additions and 1124 deletions
|
|
@ -1,13 +1,12 @@
|
|||
<script lang="ts">
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { JobCounts } from '@api';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
export let buttonTitle = 'Run';
|
||||
export let jobStatus: boolean;
|
||||
export let waitingJobCount: number;
|
||||
export let activeJobCount: number;
|
||||
export let jobCounts: JobCounts;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
|
@ -36,17 +35,23 @@
|
|||
class="overflow-y-auto rounded-md w-full max-h-[320px] block border bg-white dark:border-immich-dark-gray dark:bg-immich-dark-gray/75 dark:text-immich-dark-fg"
|
||||
>
|
||||
<tr class="text-center flex place-items-center w-full h-[60px]">
|
||||
<td class="text-sm px-2 w-1/3 text-ellipsis">{jobStatus ? 'Active' : 'Idle'}</td>
|
||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||
{#if activeJobCount !== undefined}
|
||||
{activeJobCount}
|
||||
<td class="text-sm px-2 w-1/3 text-ellipsis">
|
||||
{#if jobCounts}
|
||||
<span>{jobCounts.active > 0 || jobCounts.waiting > 0 ? 'Active' : 'Idle'}</span>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
</td>
|
||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||
{#if waitingJobCount !== undefined}
|
||||
{waitingJobCount}
|
||||
{#if jobCounts.active !== undefined}
|
||||
{jobCounts.active}
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
</td>
|
||||
<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
|
||||
{#if jobCounts.waiting !== undefined}
|
||||
{jobCounts.waiting}
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
|
|
@ -59,9 +64,9 @@
|
|||
<button
|
||||
on:click={() => dispatch('click')}
|
||||
class="px-6 py-3 text-sm bg-immich-primary dark:bg-immich-dark-primary font-medium rounded-2xl hover:bg-immich-primary/50 transition-all hover:cursor-pointer disabled:cursor-not-allowed shadow-sm text-immich-bg dark:text-immich-dark-gray"
|
||||
disabled={jobStatus}
|
||||
disabled={jobCounts.active > 0 && jobCounts.waiting > 0}
|
||||
>
|
||||
{#if jobStatus}
|
||||
{#if jobCounts.active > 0 || jobCounts.waiting > 0}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
{buttonTitle}
|
||||
|
|
|
|||
|
|
@ -8,203 +8,97 @@
|
|||
import { onDestroy, onMount } from 'svelte';
|
||||
import JobTile from './job-tile.svelte';
|
||||
|
||||
let allJobsStatus: AllJobStatusResponseDto;
|
||||
let setIntervalHandler: NodeJS.Timer;
|
||||
let jobs: AllJobStatusResponseDto;
|
||||
let timer: NodeJS.Timer;
|
||||
|
||||
const load = async () => {
|
||||
const { data } = await api.jobApi.getAllJobsStatus();
|
||||
jobs = data;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.jobApi.getAllJobsStatus();
|
||||
allJobsStatus = data;
|
||||
|
||||
setIntervalHandler = setInterval(async () => {
|
||||
const { data } = await api.jobApi.getAllJobsStatus();
|
||||
allJobsStatus = data;
|
||||
}, 1000);
|
||||
await load();
|
||||
timer = setInterval(async () => await load(), 5_000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(setIntervalHandler);
|
||||
clearInterval(timer);
|
||||
});
|
||||
|
||||
const runThumbnailGeneration = async () => {
|
||||
const run = async (jobId: JobId, jobName: string, emptyMessage: string) => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(JobId.ThumbnailGeneration, {
|
||||
command: JobCommand.Start
|
||||
});
|
||||
const { data } = await api.jobApi.sendJobCommand(jobId, { command: JobCommand.Start });
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: `Thumbnail generation job started for ${data} assets`,
|
||||
message: `Started ${jobName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
notificationController.show({
|
||||
message: `No missing thumbnails found`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[ERROR] runThumbnailGeneration', e);
|
||||
|
||||
notificationController.show({
|
||||
message: `Error running thumbnail generation job, check console for more detail`,
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const runExtractEXIF = async () => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(JobId.MetadataExtraction, {
|
||||
command: JobCommand.Start
|
||||
});
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: `Extract EXIF job started for ${data} assets`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
notificationController.show({
|
||||
message: `No missing EXIF found`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[ERROR] runExtractEXIF', e);
|
||||
|
||||
notificationController.show({
|
||||
message: `Error running extract EXIF job, check console for more detail`,
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const runMachineLearning = async () => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(JobId.MachineLearning, {
|
||||
command: JobCommand.Start
|
||||
});
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: `Object detection job started for ${data} assets`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
notificationController.show({
|
||||
message: `No missing object detection found`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({ message: emptyMessage, type: NotificationType.Info });
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, `Error running machine learning job, check console for more detail`);
|
||||
}
|
||||
};
|
||||
|
||||
const runVideoConversion = async () => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(JobId.VideoConversion, {
|
||||
command: JobCommand.Start
|
||||
});
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: `Video conversion job started for ${data} assets`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
notificationController.show({
|
||||
message: `No videos without an encoded version found`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, `Error running video conversion job, check console for more detail`);
|
||||
}
|
||||
};
|
||||
|
||||
const runTemplateMigration = async () => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(JobId.StorageTemplateMigration, {
|
||||
command: JobCommand.Start
|
||||
});
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: `Storage migration started`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
notificationController.show({
|
||||
message: `All files have been migrated to the new storage template`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[ERROR] runTemplateMigration', e);
|
||||
|
||||
notificationController.show({
|
||||
message: `Error running template migration job, check console for more detail`,
|
||||
type: NotificationType.Error
|
||||
});
|
||||
handleError(error, `Unable to start ${jobName}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-10">
|
||||
<JobTile
|
||||
title={'Generate thumbnails'}
|
||||
subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
|
||||
on:click={runThumbnailGeneration}
|
||||
jobStatus={allJobsStatus?.isThumbnailGenerationActive}
|
||||
waitingJobCount={allJobsStatus?.thumbnailGenerationQueueCount.waiting}
|
||||
activeJobCount={allJobsStatus?.thumbnailGenerationQueueCount.active}
|
||||
/>
|
||||
{#if jobs}
|
||||
<JobTile
|
||||
title={'Generate thumbnails'}
|
||||
subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
|
||||
on:click={() =>
|
||||
run(JobId.ThumbnailGeneration, 'thumbnail generation', 'No missing thumbnails found')}
|
||||
jobCounts={jobs[JobId.ThumbnailGeneration]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Extract EXIF'}
|
||||
subtitle={'Extract missing EXIF information'}
|
||||
on:click={runExtractEXIF}
|
||||
jobStatus={allJobsStatus?.isMetadataExtractionActive}
|
||||
waitingJobCount={allJobsStatus?.metadataExtractionQueueCount.waiting}
|
||||
activeJobCount={allJobsStatus?.metadataExtractionQueueCount.active}
|
||||
/>
|
||||
<JobTile
|
||||
title={'Extract EXIF'}
|
||||
subtitle={'Extract missing EXIF information'}
|
||||
on:click={() => run(JobId.MetadataExtraction, 'extract EXIF', 'No missing EXIF found')}
|
||||
jobCounts={jobs[JobId.MetadataExtraction]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Detect objects'}
|
||||
subtitle={'Run machine learning process to detect and classify objects'}
|
||||
on:click={runMachineLearning}
|
||||
jobStatus={allJobsStatus?.isMachineLearningActive}
|
||||
waitingJobCount={allJobsStatus?.machineLearningQueueCount.waiting}
|
||||
activeJobCount={allJobsStatus?.machineLearningQueueCount.active}
|
||||
>
|
||||
Note that some assets may not have any objects detected, this is normal.
|
||||
</JobTile>
|
||||
|
||||
<JobTile
|
||||
title={'Video transcoding'}
|
||||
subtitle={'Run video transcoding process to transcode videos not in the desired format'}
|
||||
on:click={runVideoConversion}
|
||||
jobStatus={allJobsStatus?.isVideoConversionActive}
|
||||
waitingJobCount={allJobsStatus?.videoConversionQueueCount.waiting}
|
||||
activeJobCount={allJobsStatus?.videoConversionQueueCount.active}
|
||||
>
|
||||
Note that some videos won't require transcoding, this is normal.
|
||||
</JobTile>
|
||||
|
||||
<JobTile
|
||||
title={'Storage migration'}
|
||||
subtitle={''}
|
||||
on:click={runTemplateMigration}
|
||||
jobStatus={allJobsStatus?.isStorageMigrationActive}
|
||||
waitingJobCount={allJobsStatus?.storageMigrationQueueCount.waiting}
|
||||
activeJobCount={allJobsStatus?.storageMigrationQueueCount.active}
|
||||
>
|
||||
Apply the current
|
||||
<a
|
||||
href="/admin/system-settings?open=storage-template"
|
||||
class="text-immich-primary dark:text-immich-dark-primary">Storage template</a
|
||||
<JobTile
|
||||
title={'Detect objects'}
|
||||
subtitle={'Run machine learning process to detect and classify objects'}
|
||||
on:click={() =>
|
||||
run(JobId.MachineLearning, 'object detection', 'No missing object detection found')}
|
||||
jobCounts={jobs[JobId.MachineLearning]}
|
||||
>
|
||||
to previously uploaded assets
|
||||
</JobTile>
|
||||
Note that some assets may not have any objects detected, this is normal.
|
||||
</JobTile>
|
||||
|
||||
<JobTile
|
||||
title={'Video transcoding'}
|
||||
subtitle={'Run video transcoding process to transcode videos not in the desired format'}
|
||||
on:click={() =>
|
||||
run(
|
||||
JobId.VideoConversion,
|
||||
'video conversion',
|
||||
'No videos without an encoded version found'
|
||||
)}
|
||||
jobCounts={jobs[JobId.MachineLearning]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Storage migration'}
|
||||
subtitle={''}
|
||||
on:click={() =>
|
||||
run(
|
||||
JobId.StorageTemplateMigration,
|
||||
'storage template migration',
|
||||
'All files have been migrated to the new storage template'
|
||||
)}
|
||||
jobCounts={jobs[JobId.StorageTemplateMigration]}
|
||||
>
|
||||
Apply the current
|
||||
<a
|
||||
href="/admin/system-settings?open=storage-template"
|
||||
class="text-immich-primary dark:text-immich-dark-primary">Storage template</a
|
||||
>
|
||||
to previously uploaded assets
|
||||
</JobTile>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue