diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md
index a137980e00..3d4ab6a892 100644
--- a/docs/docs/features/libraries.md
+++ b/docs/docs/features/libraries.md
@@ -68,7 +68,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
### Nightly job
-There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion.
+There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page.
## Usage
diff --git a/i18n/en.json b/i18n/en.json
index e35f1906c4..4f84d140e0 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -96,7 +96,7 @@
"library_scanning_enable_description": "Enable periodic library scanning",
"library_settings": "External Library",
"library_settings_description": "Manage external library settings",
- "library_tasks_description": "Perform library tasks",
+ "library_tasks_description": "Scan external libraries for new and/or changed assets",
"library_watching_enable_description": "Watch external libraries for file changes",
"library_watching_settings": "Library watching (EXPERIMENTAL)",
"library_watching_settings_description": "Automatically watch for changed files",
@@ -336,6 +336,7 @@
"untracked_files": "Untracked Files",
"untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
"user_cleanup_job": "User cleanup",
+ "cleanup": "Cleanup",
"user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
"user_delete_delay_settings": "Delete delay",
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
@@ -1114,6 +1115,7 @@
"say_something": "Say something",
"scan_all_libraries": "Scan All Libraries",
"scan_library": "Scan",
+ "rescan": "Rescan",
"scan_settings": "Scan Settings",
"scanning_for_album": "Scanning for album...",
"search": "Search",
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 676e1d27db..95168b1754 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -473,7 +473,7 @@ export enum JobName {
LIBRARY_SYNC_FILE = 'library-sync-file',
LIBRARY_SYNC_ASSET = 'library-sync-asset',
LIBRARY_DELETE = 'library-delete',
- LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all',
+ LIBRARY_QUEUE_SCAN_ALL = 'library-queue-scan-all',
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
// cleanup
diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts
index 22408c33de..167c121706 100644
--- a/server/src/services/job.service.ts
+++ b/server/src/services/job.service.ts
@@ -170,7 +170,7 @@ export class JobService extends BaseService {
}
case QueueName.LIBRARY: {
- return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } });
+ return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
}
case QueueName.BACKUP_DATABASE: {
diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts
index ded7e0630a..c869f803f0 100644
--- a/server/src/services/library.service.spec.ts
+++ b/server/src/services/library.service.spec.ts
@@ -1079,7 +1079,7 @@ describe(LibraryService.name, () => {
it('should queue the refresh job', async () => {
mocks.library.getAll.mockResolvedValue([libraryStub.externalLibrary1]);
- await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS);
+ await expect(sut.handleQueueScanAll()).resolves.toBe(JobStatus.SUCCESS);
expect(mocks.job.queue.mock.calls).toEqual([
[
diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts
index 441d130c12..cdd6a3948f 100644
--- a/server/src/services/library.service.ts
+++ b/server/src/services/library.service.ts
@@ -47,7 +47,7 @@ export class LibraryService extends BaseService {
name: 'libraryScan',
expression: scan.cronExpression,
onTick: () =>
- handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger),
+ handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger),
start: scan.enabled,
});
}
@@ -210,11 +210,17 @@ export class LibraryService extends BaseService {
@OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY })
async handleQueueCleanup(): Promise {
- this.logger.debug('Cleaning up any pending library deletions');
- const pendingDeletion = await this.libraryRepository.getAllDeleted();
- await this.jobRepository.queueAll(
- pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
- );
+ this.logger.log('Checking for any libraries pending deletion...');
+ const pendingDeletions = await this.libraryRepository.getAllDeleted();
+ if (pendingDeletions.length > 0) {
+ const libraryString = pendingDeletions.length === 1 ? 'library' : 'libraries';
+ this.logger.log(`Found ${pendingDeletions.length} ${libraryString} pending deletion, cleaning up...`);
+
+ await this.jobRepository.queueAll(
+ pendingDeletions.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })),
+ );
+ }
+
return JobStatus.SUCCESS;
}
@@ -442,9 +448,13 @@ export class LibraryService extends BaseService {
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
}
- @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, queue: QueueName.LIBRARY })
- async handleQueueSyncAll(): Promise {
- this.logger.debug(`Refreshing all external libraries`);
+ async queueScanAll() {
+ await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: {} });
+ }
+
+ @OnJob({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, queue: QueueName.LIBRARY })
+ async handleQueueScanAll(): Promise {
+ this.logger.log(`Refreshing all external libraries`);
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} });
diff --git a/server/src/types.ts b/server/src/types.ts
index 5360e519bd..902e13b9ea 100644
--- a/server/src/types.ts
+++ b/server/src/types.ts
@@ -351,7 +351,7 @@ export type JobItem =
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
| { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob }
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
- | { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
+ | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data?: IBaseJob }
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
// Notification
diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte
index 0e39647c75..80dd29e0be 100644
--- a/web/src/lib/components/admin-page/jobs/job-tile.svelte
+++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte
@@ -185,7 +185,7 @@
{#if !disabled && !multipleButtons && isIdle}
onCommand({ command: JobCommand.Start, force: false })}>
- {$t('start').toUpperCase()}
+ {missingText}
{/if}
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
index 9b4f3ffdd6..4eb0bf6bb0 100644
--- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
+++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
@@ -79,8 +79,7 @@
icon: mdiLibraryShelves,
title: $getJobName(JobName.Library),
subtitle: $t('admin.library_tasks_description'),
- allText: $t('all'),
- missingText: $t('refresh'),
+ missingText: $t('rescan'),
},
[JobName.Sidecar]: {
title: $getJobName(JobName.Sidecar),
@@ -135,14 +134,14 @@
[JobName.StorageTemplateMigration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.StorageTemplateMigration),
- missingText: $t('missing'),
+ missingText: $t('start'),
description: StorageMigrationDescription,
},
[JobName.Migration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.Migration),
subtitle: $t('admin.migration_job_description'),
- missingText: $t('missing'),
+ missingText: $t('start'),
},
};
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index c87b623549..7d542a940a 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -146,7 +146,7 @@ export const getJobName = derived(t, ($t) => {
[JobName.Migration]: $t('admin.migration_job'),
[JobName.BackgroundTask]: $t('admin.background_task_job'),
[JobName.Search]: $t('search'),
- [JobName.Library]: $t('library'),
+ [JobName.Library]: $t('external_libraries'),
[JobName.Notifications]: $t('notifications'),
[JobName.BackupDatabase]: $t('admin.backup_database'),
};
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
index 04325f9fc2..c397fe6d3a 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -22,7 +22,10 @@
getAllLibraries,
getLibraryStatistics,
getUserAdmin,
+ JobCommand,
+ JobName,
scanLibrary,
+ sendJobCommand,
updateLibrary,
type LibraryResponseDto,
type LibraryStatsResponseDto,
@@ -151,9 +154,8 @@
const handleScanAll = async () => {
try {
- for (const library of libraries) {
- await scanLibrary({ id: library.id });
- }
+ await sendJobCommand({ id: JobName.Library, jobCommandDto: { command: JobCommand.Start } });
+
notificationController.show({
message: $t('admin.refreshing_all_libraries'),
type: NotificationType.Info,