mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web/server): Add options to rerun job on all assets (#1422)
This commit is contained in:
		
							
								
								
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@@ -1203,6 +1203,12 @@ export interface JobCommandDto {
 | 
			
		||||
     * @memberof JobCommandDto
 | 
			
		||||
     */
 | 
			
		||||
    'command': JobCommand;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {boolean}
 | 
			
		||||
     * @memberof JobCommandDto
 | 
			
		||||
     */
 | 
			
		||||
    'includeAllAssets': boolean;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 
 | 
			
		||||
@@ -101,4 +101,8 @@ input:focus-visible {
 | 
			
		||||
		display: none;
 | 
			
		||||
		scrollbar-width: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.job-play-button {
 | 
			
		||||
		@apply h-full flex flex-col place-items-center place-content-center px-8 text-gray-600 transition-all hover:bg-immich-primary hover:text-white dark:text-gray-200 dark:hover:bg-immich-dark-primary text-sm dark:hover:text-black w-[120px] gap-2;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,76 +1,102 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
			
		||||
	import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte';
 | 
			
		||||
	import Play from 'svelte-material-icons/Play.svelte';
 | 
			
		||||
	import AllInclusive from 'svelte-material-icons/AllInclusive.svelte';
 | 
			
		||||
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	import { JobCounts } from '@api';
 | 
			
		||||
 | 
			
		||||
	export let title: string;
 | 
			
		||||
	export let subtitle: string;
 | 
			
		||||
	export let buttonTitle = 'Run';
 | 
			
		||||
	export let jobCounts: JobCounts;
 | 
			
		||||
	/**
 | 
			
		||||
	 * Show options to run job on all assets of just missing ones
 | 
			
		||||
	 */
 | 
			
		||||
	export let showOptions = true;
 | 
			
		||||
 | 
			
		||||
	$: isRunning = jobCounts.active > 0 || jobCounts.waiting > 0;
 | 
			
		||||
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
	const run = (includeAllAssets: boolean) => {
 | 
			
		||||
		dispatch('click', { includeAllAssets });
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex border-b pb-5 dark:border-b-immich-dark-gray">
 | 
			
		||||
	<div class="w-[70%]">
 | 
			
		||||
		<h1 class="text-immich-primary dark:text-immich-dark-primary text-sm font-semibold">
 | 
			
		||||
			{title.toUpperCase()}
 | 
			
		||||
		</h1>
 | 
			
		||||
		<p class="text-sm mt-1 dark:text-immich-dark-fg">{subtitle}</p>
 | 
			
		||||
		<p class="text-sm dark:text-immich-dark-fg">
 | 
			
		||||
			<slot />
 | 
			
		||||
		</p>
 | 
			
		||||
		<table class="text-left w-full mt-5">
 | 
			
		||||
			<!-- table header -->
 | 
			
		||||
			<thead
 | 
			
		||||
				class="border rounded-md mb-2 dark:bg-immich-dark-gray dark:border-immich-dark-gray bg-immich-primary/10 flex text-immich-primary dark:text-immich-dark-primary w-full h-12"
 | 
			
		||||
			>
 | 
			
		||||
				<tr class="flex w-full place-items-center">
 | 
			
		||||
					<th class="text-center w-1/3 font-medium text-sm">Status</th>
 | 
			
		||||
					<th class="text-center w-1/3 font-medium text-sm">Active</th>
 | 
			
		||||
					<th class="text-center w-1/3 font-medium text-sm">Waiting</th>
 | 
			
		||||
				</tr>
 | 
			
		||||
			</thead>
 | 
			
		||||
			<tbody
 | 
			
		||||
				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">
 | 
			
		||||
						{#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">
 | 
			
		||||
<div class="flex justify-between rounded-3xl bg-gray-100 dark:bg-immich-dark-gray">
 | 
			
		||||
	<div id="job-info" class="w-[70%] p-9">
 | 
			
		||||
		<div class="flex flex-col gap-2">
 | 
			
		||||
			<div class="text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
				{title.toUpperCase()}
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{#if subtitle.length > 0}
 | 
			
		||||
				<div class="text-sm dark:text-white">{subtitle}</div>
 | 
			
		||||
			{/if}
 | 
			
		||||
			<div class="text-sm dark:text-white"><slot /></div>
 | 
			
		||||
 | 
			
		||||
			<div class="flex w-full mt-4">
 | 
			
		||||
				<div
 | 
			
		||||
					class="flex place-items-center justify-between bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray w-full rounded-tl-lg rounded-bl-lg py-4 pl-4 pr-6"
 | 
			
		||||
				>
 | 
			
		||||
					<p>Active</p>
 | 
			
		||||
					<p class="text-2xl">
 | 
			
		||||
						{#if jobCounts.active !== undefined}
 | 
			
		||||
							{jobCounts.active}
 | 
			
		||||
						{:else}
 | 
			
		||||
							<LoadingSpinner />
 | 
			
		||||
						{/if}
 | 
			
		||||
					</td>
 | 
			
		||||
					<td class="flex justify-center text-sm px-2 w-1/3 text-ellipsis">
 | 
			
		||||
					</p>
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
				<div
 | 
			
		||||
					class="flex place-items-center justify-between bg-gray-200 text-immich-dark-bg dark:bg-gray-700 dark:text-immich-gray w-full rounded-tr-lg rounded-br-lg py-4 pr-4 pl-6"
 | 
			
		||||
				>
 | 
			
		||||
					<p class="text-2xl">
 | 
			
		||||
						{#if jobCounts.waiting !== undefined}
 | 
			
		||||
							{jobCounts.waiting}
 | 
			
		||||
						{:else}
 | 
			
		||||
							<LoadingSpinner />
 | 
			
		||||
						{/if}
 | 
			
		||||
					</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
			</tbody>
 | 
			
		||||
		</table>
 | 
			
		||||
					</p>
 | 
			
		||||
					<p>Waiting</p>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="w-[30%] flex place-items-center place-content-end">
 | 
			
		||||
		<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={jobCounts.active > 0 && jobCounts.waiting > 0}
 | 
			
		||||
		>
 | 
			
		||||
			{#if jobCounts.active > 0 || jobCounts.waiting > 0}
 | 
			
		||||
	<div id="job-action" class="flex flex-col">
 | 
			
		||||
		{#if isRunning}
 | 
			
		||||
			<button
 | 
			
		||||
				class="job-play-button bg-gray-300/90 dark:bg-gray-600/90 rounded-br-3xl rounded-tr-3xl disabled:cursor-not-allowed"
 | 
			
		||||
				disabled
 | 
			
		||||
			>
 | 
			
		||||
				<LoadingSpinner />
 | 
			
		||||
			</button>
 | 
			
		||||
		{/if}
 | 
			
		||||
 | 
			
		||||
		{#if !isRunning}
 | 
			
		||||
			{#if showOptions}
 | 
			
		||||
				<button
 | 
			
		||||
					class="job-play-button bg-gray-300 dark:bg-gray-600 rounded-tr-3xl"
 | 
			
		||||
					on:click={() => run(true)}
 | 
			
		||||
				>
 | 
			
		||||
					<AllInclusive size="18" /> ALL
 | 
			
		||||
				</button>
 | 
			
		||||
				<button
 | 
			
		||||
					class="job-play-button bg-gray-300/90 dark:bg-gray-600/90 rounded-br-3xl"
 | 
			
		||||
					on:click={() => run(false)}
 | 
			
		||||
				>
 | 
			
		||||
					<SelectionSearch size="18" /> MISSING
 | 
			
		||||
				</button>
 | 
			
		||||
			{:else}
 | 
			
		||||
				{buttonTitle}
 | 
			
		||||
				<button
 | 
			
		||||
					class="job-play-button bg-gray-300/90 dark:bg-gray-600/90 rounded-br-3xl rounded-tr-3xl"
 | 
			
		||||
					on:click={() => run(true)}
 | 
			
		||||
				>
 | 
			
		||||
					<Play size="48" />
 | 
			
		||||
				</button>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</button>
 | 
			
		||||
		{/if}
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,20 +18,28 @@
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		await load();
 | 
			
		||||
		timer = setInterval(async () => await load(), 5_000);
 | 
			
		||||
		timer = setInterval(async () => await load(), 1_000);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	onDestroy(() => {
 | 
			
		||||
		clearInterval(timer);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const run = async (jobId: JobId, jobName: string, emptyMessage: string) => {
 | 
			
		||||
	const run = async (
 | 
			
		||||
		jobId: JobId,
 | 
			
		||||
		jobName: string,
 | 
			
		||||
		emptyMessage: string,
 | 
			
		||||
		includeAllAssets: boolean
 | 
			
		||||
	) => {
 | 
			
		||||
		try {
 | 
			
		||||
			const { data } = await api.jobApi.sendJobCommand(jobId, { command: JobCommand.Start });
 | 
			
		||||
			const { data } = await api.jobApi.sendJobCommand(jobId, {
 | 
			
		||||
				command: JobCommand.Start,
 | 
			
		||||
				includeAllAssets
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (data) {
 | 
			
		||||
				notificationController.show({
 | 
			
		||||
					message: `Started ${jobName}`,
 | 
			
		||||
					message: includeAllAssets ? `Started ${jobName} for all assets` : `Started ${jobName}`,
 | 
			
		||||
					type: NotificationType.Info
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -43,53 +51,77 @@
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="flex flex-col gap-10">
 | 
			
		||||
<div class="flex flex-col gap-7">
 | 
			
		||||
	{#if jobs}
 | 
			
		||||
		<JobTile
 | 
			
		||||
			title={'Generate thumbnails'}
 | 
			
		||||
			subtitle={'Regenerate missing thumbnail (JPEG, WEBP)'}
 | 
			
		||||
			on:click={() =>
 | 
			
		||||
				run(JobId.ThumbnailGeneration, 'thumbnail generation', 'No missing thumbnails found')}
 | 
			
		||||
			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]}
 | 
			
		||||
		/>
 | 
			
		||||
 | 
			
		||||
		<JobTile
 | 
			
		||||
			title={'Extract EXIF'}
 | 
			
		||||
			subtitle={'Extract missing EXIF information'}
 | 
			
		||||
			on:click={() => run(JobId.MetadataExtraction, 'extract EXIF', 'No missing EXIF found')}
 | 
			
		||||
			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]}
 | 
			
		||||
		/>
 | 
			
		||||
 | 
			
		||||
		<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')}
 | 
			
		||||
			on:click={(e) => {
 | 
			
		||||
				const { includeAllAssets } = e.detail;
 | 
			
		||||
 | 
			
		||||
				run(
 | 
			
		||||
					JobId.MachineLearning,
 | 
			
		||||
					'object detection',
 | 
			
		||||
					'No missing object detection found',
 | 
			
		||||
					includeAllAssets
 | 
			
		||||
				);
 | 
			
		||||
			}}
 | 
			
		||||
			jobCounts={jobs[JobId.MachineLearning]}
 | 
			
		||||
		>
 | 
			
		||||
			Note that some assets may not have any objects detected, this is normal.
 | 
			
		||||
			Note that some assets may not have any objects detected
 | 
			
		||||
		</JobTile>
 | 
			
		||||
 | 
			
		||||
		<JobTile
 | 
			
		||||
			title={'Video transcoding'}
 | 
			
		||||
			subtitle={'Run video transcoding process to transcode videos not in the desired format'}
 | 
			
		||||
			on:click={() =>
 | 
			
		||||
			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'
 | 
			
		||||
				)}
 | 
			
		||||
					'No videos without an encoded version found',
 | 
			
		||||
					includeAllAssets
 | 
			
		||||
				);
 | 
			
		||||
			}}
 | 
			
		||||
			jobCounts={jobs[JobId.VideoConversion]}
 | 
			
		||||
		/>
 | 
			
		||||
 | 
			
		||||
		<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'
 | 
			
		||||
					'All files have been migrated to the new storage template',
 | 
			
		||||
					false
 | 
			
		||||
				)}
 | 
			
		||||
			jobCounts={jobs[JobId.StorageTemplateMigration]}
 | 
			
		||||
		>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user