You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-06-19 04:07:43 +02:00
refactor(server): jobs (#2023)
* refactor: job to domain * chore: regenerate open api * chore: tests * fix: missing breaks * fix: get asset with missing exif data --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
90
web/src/api/open-api/api.ts
generated
90
web/src/api/open-api/api.ts
generated
@ -291,34 +291,52 @@ export interface AlbumResponseDto {
|
||||
export interface AllJobStatusResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {JobCounts}
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'thumbnail-generation': JobCounts;
|
||||
'thumbnail-generation-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCounts}
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'metadata-extraction': JobCounts;
|
||||
'metadata-extraction-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCounts}
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'video-conversion': JobCounts;
|
||||
'video-conversion-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCounts}
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'machine-learning': JobCounts;
|
||||
'object-tagging-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCounts}
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'storage-template-migration': JobCounts;
|
||||
'clip-encoding-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'storage-template-migration-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'background-task-queue': JobCountsDto;
|
||||
/**
|
||||
*
|
||||
* @type {JobCountsDto}
|
||||
* @memberof AllJobStatusResponseDto
|
||||
*/
|
||||
'search-queue': JobCountsDto;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -1203,7 +1221,8 @@ export interface GetAssetCountByTimeBucketDto {
|
||||
|
||||
export const JobCommand = {
|
||||
Start: 'start',
|
||||
Stop: 'stop'
|
||||
Pause: 'pause',
|
||||
Empty: 'empty'
|
||||
} as const;
|
||||
|
||||
export type JobCommand = typeof JobCommand[keyof typeof JobCommand];
|
||||
@ -1226,42 +1245,42 @@ export interface JobCommandDto {
|
||||
* @type {boolean}
|
||||
* @memberof JobCommandDto
|
||||
*/
|
||||
'includeAllAssets': boolean;
|
||||
'force': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface JobCounts
|
||||
* @interface JobCountsDto
|
||||
*/
|
||||
export interface JobCounts {
|
||||
export interface JobCountsDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof JobCounts
|
||||
* @memberof JobCountsDto
|
||||
*/
|
||||
'active': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof JobCounts
|
||||
* @memberof JobCountsDto
|
||||
*/
|
||||
'completed': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof JobCounts
|
||||
* @memberof JobCountsDto
|
||||
*/
|
||||
'failed': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof JobCounts
|
||||
* @memberof JobCountsDto
|
||||
*/
|
||||
'delayed': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof JobCounts
|
||||
* @memberof JobCountsDto
|
||||
*/
|
||||
'waiting': number;
|
||||
}
|
||||
@ -1271,15 +1290,18 @@ export interface JobCounts {
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const JobId = {
|
||||
ThumbnailGeneration: 'thumbnail-generation',
|
||||
MetadataExtraction: 'metadata-extraction',
|
||||
VideoConversion: 'video-conversion',
|
||||
MachineLearning: 'machine-learning',
|
||||
StorageTemplateMigration: 'storage-template-migration'
|
||||
export const JobName = {
|
||||
ThumbnailGenerationQueue: 'thumbnail-generation-queue',
|
||||
MetadataExtractionQueue: 'metadata-extraction-queue',
|
||||
VideoConversionQueue: 'video-conversion-queue',
|
||||
ObjectTaggingQueue: 'object-tagging-queue',
|
||||
ClipEncodingQueue: 'clip-encoding-queue',
|
||||
BackgroundTaskQueue: 'background-task-queue',
|
||||
StorageTemplateMigrationQueue: 'storage-template-migration-queue',
|
||||
SearchQueue: 'search-queue'
|
||||
} as const;
|
||||
|
||||
export type JobId = typeof JobId[keyof typeof JobId];
|
||||
export type JobName = typeof JobName[keyof typeof JobName];
|
||||
|
||||
|
||||
/**
|
||||
@ -6169,12 +6191,12 @@ export const JobApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {JobId} jobId
|
||||
* @param {JobName} jobId
|
||||
* @param {JobCommandDto} jobCommandDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sendJobCommand: async (jobId: JobId, jobCommandDto: JobCommandDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
sendJobCommand: async (jobId: JobName, jobCommandDto: JobCommandDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'jobId' is not null or undefined
|
||||
assertParamExists('sendJobCommand', 'jobId', jobId)
|
||||
// verify required parameter 'jobCommandDto' is not null or undefined
|
||||
@ -6233,12 +6255,12 @@ export const JobApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {JobId} jobId
|
||||
* @param {JobName} jobId
|
||||
* @param {JobCommandDto} jobCommandDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<number>> {
|
||||
async sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.sendJobCommand(jobId, jobCommandDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
@ -6262,12 +6284,12 @@ export const JobApiFactory = function (configuration?: Configuration, basePath?:
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {JobId} jobId
|
||||
* @param {JobName} jobId
|
||||
* @param {JobCommandDto} jobCommandDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: any): AxiosPromise<number> {
|
||||
sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: any): AxiosPromise<void> {
|
||||
return localVarFp.sendJobCommand(jobId, jobCommandDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
@ -6292,13 +6314,13 @@ export class JobApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JobId} jobId
|
||||
* @param {JobName} jobId
|
||||
* @param {JobCommandDto} jobCommandDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof JobApi
|
||||
*/
|
||||
public sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig) {
|
||||
public sendJobCommand(jobId: JobName, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig) {
|
||||
return JobApiFp(this.configuration).sendJobCommand(jobId, jobCommandDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,11 @@
|
||||
import AllInclusive from 'svelte-material-icons/AllInclusive.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { JobCounts } from '@api';
|
||||
import { JobCountsDto } from '@api';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
export let jobCounts: JobCounts;
|
||||
export let jobCounts: JobCountsDto;
|
||||
/**
|
||||
* Show options to run job on all assets of just missing ones
|
||||
*/
|
||||
@ -19,8 +19,8 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const run = (includeAllAssets: boolean) => {
|
||||
dispatch('click', { includeAllAssets });
|
||||
const run = (force: boolean) => {
|
||||
dispatch('click', { force });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AllJobStatusResponseDto, api, JobCommand, JobId } from '@api';
|
||||
import { AllJobStatusResponseDto, api, JobCommand, JobName } from '@api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import JobTile from './job-tile.svelte';
|
||||
|
||||
@ -18,35 +18,42 @@
|
||||
|
||||
onMount(async () => {
|
||||
await load();
|
||||
timer = setInterval(async () => await load(), 1_000);
|
||||
timer = setInterval(async () => await load(), 5_000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
|
||||
const run = async (
|
||||
jobId: JobId,
|
||||
jobName: string,
|
||||
emptyMessage: string,
|
||||
includeAllAssets: boolean
|
||||
) => {
|
||||
try {
|
||||
const { data } = await api.jobApi.sendJobCommand(jobId, {
|
||||
command: JobCommand.Start,
|
||||
includeAllAssets
|
||||
});
|
||||
function getJobLabel(jobName: JobName) {
|
||||
const names: Record<JobName, string> = {
|
||||
[JobName.ThumbnailGenerationQueue]: 'Generate Thumbnails',
|
||||
[JobName.MetadataExtractionQueue]: 'Extract Metadata',
|
||||
[JobName.VideoConversionQueue]: 'Transcode Videos',
|
||||
[JobName.ObjectTaggingQueue]: 'Tag Objects',
|
||||
[JobName.ClipEncodingQueue]: 'Clip Encoding',
|
||||
[JobName.BackgroundTaskQueue]: 'Background Task',
|
||||
[JobName.StorageTemplateMigrationQueue]: 'Storage Template Migration',
|
||||
[JobName.SearchQueue]: 'Search'
|
||||
};
|
||||
|
||||
if (data) {
|
||||
notificationController.show({
|
||||
message: includeAllAssets ? `Started ${jobName} for all assets` : `Started ${jobName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} else {
|
||||
notificationController.show({ message: emptyMessage, type: NotificationType.Info });
|
||||
}
|
||||
return names[jobName];
|
||||
}
|
||||
|
||||
const start = async (jobId: JobName, force: boolean) => {
|
||||
const label = getJobLabel(jobId);
|
||||
|
||||
try {
|
||||
await api.jobApi.sendJobCommand(jobId, { command: JobCommand.Start, force });
|
||||
|
||||
jobs[jobId].active += 1;
|
||||
|
||||
notificationController.show({
|
||||
message: `Started job: ${label}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, `Unable to start ${jobName}`);
|
||||
handleError(error, `Unable to start job: ${label}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -54,76 +61,48 @@
|
||||
<div class="flex flex-col gap-7">
|
||||
{#if jobs}
|
||||
<JobTile
|
||||
title={'Generate thumbnails'}
|
||||
subtitle={'Regenerate JPEG and WebP thumbnails'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
|
||||
run(
|
||||
JobId.ThumbnailGeneration,
|
||||
'thumbnail generation',
|
||||
'No missing thumbnails found',
|
||||
includeAllAssets
|
||||
);
|
||||
}}
|
||||
jobCounts={jobs[JobId.ThumbnailGeneration]}
|
||||
title="Generate thumbnails"
|
||||
subtitle="Regenerate JPEG and WebP thumbnails"
|
||||
on:click={(e) => start(JobName.ThumbnailGenerationQueue, e.detail.force)}
|
||||
jobCounts={jobs[JobName.ThumbnailGenerationQueue]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'EXTRACT METADATA'}
|
||||
subtitle={'Extract metadata information i.e. GPS, resolution...etc'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
run(JobId.MetadataExtraction, 'extract EXIF', 'No missing EXIF found', includeAllAssets);
|
||||
}}
|
||||
jobCounts={jobs[JobId.MetadataExtraction]}
|
||||
title="Extract Metadata"
|
||||
subtitle="Extract metadata information i.e. GPS, resolution...etc"
|
||||
on:click={(e) => start(JobName.MetadataExtractionQueue, e.detail.force)}
|
||||
jobCounts={jobs[JobName.MetadataExtractionQueue]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Detect objects'}
|
||||
subtitle={'Run machine learning process to detect and classify objects'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
|
||||
run(
|
||||
JobId.MachineLearning,
|
||||
'object detection',
|
||||
'No missing object detection found',
|
||||
includeAllAssets
|
||||
);
|
||||
}}
|
||||
jobCounts={jobs[JobId.MachineLearning]}
|
||||
title="Tag Objects"
|
||||
subtitle="Run machine learning to tag objects"
|
||||
on:click={(e) => start(JobName.ObjectTaggingQueue, e.detail.force)}
|
||||
jobCounts={jobs[JobName.ObjectTaggingQueue]}
|
||||
>
|
||||
Note that some assets may not have any objects detected
|
||||
</JobTile>
|
||||
|
||||
<JobTile
|
||||
title={'Video transcoding'}
|
||||
subtitle={'Transcode videos not in the desired format'}
|
||||
on:click={(e) => {
|
||||
const { includeAllAssets } = e.detail;
|
||||
run(
|
||||
JobId.VideoConversion,
|
||||
'video conversion',
|
||||
'No videos without an encoded version found',
|
||||
includeAllAssets
|
||||
);
|
||||
}}
|
||||
jobCounts={jobs[JobId.VideoConversion]}
|
||||
title="Encode Clip"
|
||||
subtitle="Run machine learning to generate clip embeddings"
|
||||
on:click={(e) => start(JobName.ClipEncodingQueue, e.detail.force)}
|
||||
jobCounts={jobs[JobName.ClipEncodingQueue]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title={'Storage migration'}
|
||||
title="Transcode Videos"
|
||||
subtitle="Transcode videos not in the desired format"
|
||||
on:click={(e) => start(JobName.VideoConversionQueue, e.detail.force)}
|
||||
jobCounts={jobs[JobName.VideoConversionQueue]}
|
||||
/>
|
||||
|
||||
<JobTile
|
||||
title="Storage migration"
|
||||
showOptions={false}
|
||||
subtitle={''}
|
||||
on:click={() =>
|
||||
run(
|
||||
JobId.StorageTemplateMigration,
|
||||
'storage template migration',
|
||||
'All files have been migrated to the new storage template',
|
||||
false
|
||||
)}
|
||||
jobCounts={jobs[JobId.StorageTemplateMigration]}
|
||||
on:click={(e) => start(JobName.StorageTemplateMigrationQueue, e.detail.force)}
|
||||
jobCounts={jobs[JobName.StorageTemplateMigrationQueue]}
|
||||
>
|
||||
Apply the current
|
||||
<a
|
||||
|
Reference in New Issue
Block a user