From 869839f64256927c8920a6cda2d410922603b150 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Mon, 3 Mar 2025 04:29:02 +0100 Subject: [PATCH] feat(server): library cleanup from ui (#16226) * feat(server,web): scan all libraries from frontend * feat(server,web): scan all libraries from frontend * Add button text --- docs/docs/features/libraries.md | 2 +- i18n/en.json | 4 ++- server/src/enum.ts | 2 +- server/src/services/job.service.ts | 2 +- server/src/services/library.service.spec.ts | 2 +- server/src/services/library.service.ts | 28 +++++++++++++------ server/src/types.ts | 2 +- .../admin-page/jobs/job-tile.svelte | 2 +- .../admin-page/jobs/jobs-panel.svelte | 7 ++--- web/src/lib/utils.ts | 2 +- .../admin/library-management/+page.svelte | 8 ++++-- 11 files changed, 37 insertions(+), 24 deletions(-) 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,