From 1dec8ae12289638ebbc3d9c763071f7804b92935 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 21 Sep 2024 14:02:57 -0500 Subject: [PATCH] Update:Added string localization for tasks #3303 #3352 --- .../components/cards/ItemTaskRunningCard.vue | 39 +++++++++++++++-- client/plugins/i18n.js | 4 +- client/plugins/utils.js | 5 ++- client/strings/en-us.json | 32 ++++++++++++++ server/Watcher.js | 6 ++- server/managers/AbMergeManager.js | 7 +-- server/managers/AudioMetadataManager.js | 43 ++++++++++++++++--- server/managers/PodcastManager.js | 19 ++++++-- server/managers/TaskManager.js | 2 +- server/objects/Task.js | 35 +++++++-------- server/scanner/LibraryScan.js | 40 ++++++----------- server/scanner/LibraryScanner.js | 40 ++++++++++++----- server/scanner/Scanner.js | 24 ++++++++--- 13 files changed, 213 insertions(+), 83 deletions(-) diff --git a/client/components/cards/ItemTaskRunningCard.vue b/client/components/cards/ItemTaskRunningCard.vue index 14972df9..12d1b618 100644 --- a/client/components/cards/ItemTaskRunningCard.vue +++ b/client/components/cards/ItemTaskRunningCard.vue @@ -8,6 +8,7 @@

{{ title }}

{{ description }}

+

{{ specialMessage }}

{{ failedMessage }}

Canceling...

@@ -26,7 +27,16 @@ export default { }, data() { return { - cancelingScan: false + cancelingScan: false, + specialMessage: '' + } + }, + watch: { + task: { + immediate: true, + handler() { + this.initTask() + } } }, computed: { @@ -34,14 +44,17 @@ export default { return this.$store.getters['user/getIsAdminOrUp'] }, title() { + if (this.task.titleKey && this.$strings[this.task.titleKey]) { + return this.$getString(this.task.titleKey, this.task.titleSubs) + } return this.task.title || 'No Title' }, description() { + if (this.task.descriptionKey && this.$strings[this.task.descriptionKey]) { + return this.$getString(this.task.descriptionKey, this.task.descriptionSubs) + } return this.task.description || '' }, - details() { - return this.task.details || 'Unknown' - }, isFinished() { return !!this.task.isFinished }, @@ -52,6 +65,9 @@ export default { return this.isFinished && !this.isFailed }, failedMessage() { + if (this.task.errorKey && this.$strings[this.task.errorKey]) { + return this.$getString(this.task.errorKey, this.task.errorSubs) + } return this.task.error || '' }, action() { @@ -87,6 +103,21 @@ export default { } }, methods: { + initTask() { + // special message for library scan tasks + if (this.task?.data?.scanResults) { + const scanResults = this.task.data.scanResults + const strs = [] + if (scanResults.added) strs.push(this.$getString('MessageTaskScanItemsAdded', [scanResults.added])) + if (scanResults.updated) strs.push(this.$getString('MessageTaskScanItemsUpdated', [scanResults.updated])) + if (scanResults.missing) strs.push(this.$getString('MessageTaskScanItemsMissing', [scanResults.missing])) + const changesDetected = strs.length > 0 ? strs.join(', ') : this.$strings.MessageTaskScanNoChangesNeeded + const timeElapsed = scanResults.elapsed ? ` (${this.$elapsedPretty(scanResults.elapsed / 1000, false, true)})` : '' + this.specialMessage = `${changesDetected}${timeElapsed}` + } else { + this.specialMessage = '' + } + }, cancelScan() { const libraryId = this.task?.data?.libraryId if (!libraryId) { diff --git a/client/plugins/i18n.js b/client/plugins/i18n.js index 2eb6b123..0ec5ccce 100644 --- a/client/plugins/i18n.js +++ b/client/plugins/i18n.js @@ -89,10 +89,10 @@ Vue.prototype.$strings = { ...enUsStrings } * Get string and substitute * * @param {string} key - * @param {string[]} subs + * @param {string[]} [subs=[]] * @returns {string} */ -Vue.prototype.$getString = (key, subs) => { +Vue.prototype.$getString = (key, subs = []) => { if (!Vue.prototype.$strings[key]) return '' if (subs?.length && Array.isArray(subs)) { return supplant(Vue.prototype.$strings[key], subs) diff --git a/client/plugins/utils.js b/client/plugins/utils.js index 160ff943..ad08ebf6 100644 --- a/client/plugins/utils.js +++ b/client/plugins/utils.js @@ -18,7 +18,10 @@ Vue.prototype.$bytesPretty = (bytes, decimals = 2) => { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } -Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => { +Vue.prototype.$elapsedPretty = (seconds, useFullNames = false, useMilliseconds = false) => { + if (useMilliseconds && seconds > 0 && seconds < 1) { + return `${Math.floor(seconds * 1000)} ms` + } if (seconds < 60) { return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}` } diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 9e1643e1..6da92f6f 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -777,6 +777,38 @@ "MessageShareExpiresIn": "Expires in {0}", "MessageShareURLWillBe": "Share URL will be {0}", "MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?", + "MessageTaskAudioFileNotWritable": "Audio file \"{0}\" is not writable", + "MessageTaskCanceledByUser": "Task canceled by user", + "MessageTaskDownloadingEpisodeDescription": "Downloading episode \"{0}\"", + "MessageTaskEmbeddingMetadata": "Embedding metadata", + "MessageTaskEmbeddingMetadataDescription": "Embedding metadata in audiobook \"{0}\"", + "MessageTaskEncodingM4b": "Encoding M4B", + "MessageTaskEncodingM4bDescription": "Encoding audiobook \"{0}\" into a single m4b file", + "MessageTaskFailed": "Failed", + "MessageTaskFailedToBackupAudioFile": "Failed to backup audio file \"{0}\"", + "MessageTaskFailedToCreateCacheDirectory": "Failed to create cache directory", + "MessageTaskFailedToEmbedMetadataInFile": "Failed to embed metadata in file \"{0}\"", + "MessageTaskFailedToMergeAudioFiles": "Failed to merge audio files", + "MessageTaskFailedToMoveM4bFile": "Failed to move m4b file", + "MessageTaskFailedToWriteMetadataFile": "Failed to write metadata file", + "MessageTaskMatchingBooksInLibrary": "Matching books in library \"{0}\"", + "MessageTaskNoFilesToScan": "No files to scan", + "MessageTaskOpmlImport": "OPML import", + "MessageTaskOpmlImportDescription": "Creating podcasts from {0} RSS feeds", + "MessageTaskOpmlImportFeed": "OPML import feed", + "MessageTaskOpmlImportFeedDescription": "Importing RSS feed \"{0}\"", + "MessageTaskOpmlImportFeedFailed": "Failed to get podcast feed", + "MessageTaskOpmlImportFeedPodcastDescription": "Creating podcast \"{0}\"", + "MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path", + "MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast", + "MessageTaskOpmlImportFinished": "Added {0} podcasts", + "MessageTaskScanItemsAdded": "{0} added", + "MessageTaskScanItemsMissing": "{0} missing", + "MessageTaskScanItemsUpdated": "{0} updated", + "MessageTaskScanNoChangesNeeded": "No changes needed", + "MessageTaskScanningFileChanges": "Scanning file changes in \"{0}\"", + "MessageTaskScanningLibrary": "Scanning \"{0}\" library", + "MessageTaskTargetDirectoryNotWritable": "Target directory is not writable", "MessageThinking": "Thinking...", "MessageUploaderItemFailed": "Failed to upload", "MessageUploaderItemSuccess": "Successfully Uploaded!", diff --git a/server/Watcher.js b/server/Watcher.js index 0a5867bd..0e34fc66 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -335,7 +335,11 @@ class FolderWatcher extends EventEmitter { if (this.pendingFileUpdates.length) { LibraryScanner.scanFilesChanged(this.pendingFileUpdates, this.pendingTask) } else { - this.pendingTask.setFinished('Scan abandoned. No files to scan.') + const taskFinishedString = { + text: 'No files to scan', + key: 'MessageTaskNoFilesToScan' + } + this.pendingTask.setFinished(taskFinishedString) TaskManager.taskFinished(this.pendingTask) } this.pendingTask = null diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index 1fed95a1..ea70d73c 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -188,10 +188,11 @@ class AbMergeManager { if (error.message === 'FFMPEG_CANCELED') { Logger.info(`[AbMergeManager] Task cancelled ${task.id}`) } else { - Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`) + Logger.error(`[AbMergeManager] Failed to embed metadata in file "${task.data.tempFilepath}"`) const taskFailedString = { - text: 'Failed to write metadata to m4b file', - key: 'MessageTaskFailedToWriteMetadataToM4bFile' + text: `Failed to embed metadata in file ${Path.basename(task.data.tempFilepath)}`, + key: 'MessageTaskFailedToEmbedMetadataInFile', + subs: [Path.basename(task.data.tempFilepath)] } task.setFailed(taskFailedString) this.removeTask(task, true) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 8cd8039c..7911178e 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -121,6 +121,10 @@ class AudioMetadataMangaer { } } + /** + * + * @param {import('../objects/Task')} task + */ async runMetadataEmbed(task) { this.tasksRunning.push(task) TaskManager.addTask(task) @@ -132,7 +136,11 @@ class AudioMetadataMangaer { Logger.debug(`[AudioMetadataManager] Target directory ${task.data.libraryItemDir} writable: ${targetDirWritable}`) if (!targetDirWritable) { Logger.error(`[AudioMetadataManager] Target directory is not writable: ${task.data.libraryItemDir}`) - task.setFailedText('Target directory is not writable') + const taskFailedString = { + text: 'Target directory is not writable', + key: 'MessageTaskTargetDirectoryNotWritable' + } + task.setFailed(taskFailedString) this.handleTaskFinished(task) return } @@ -143,7 +151,12 @@ class AudioMetadataMangaer { await fs.access(af.path, fs.constants.W_OK) } catch (err) { Logger.error(`[AudioMetadataManager] Audio file is not writable: ${af.path}`) - task.setFailedText(`Audio file "${Path.basename(af.path)}" is not writable`) + const taskFailedString = { + text: `Audio file "${Path.basename(af.path)}" is not writable`, + key: 'MessageTaskAudioFileNotWritable', + subs: [Path.basename(af.path)] + } + task.setFailed(taskFailedString) this.handleTaskFinished(task) return } @@ -157,7 +170,11 @@ class AudioMetadataMangaer { cacheDirCreated = true } catch (err) { Logger.error(`[AudioMetadataManager] Failed to create cache directory ${task.data.itemCachePath}`, err) - task.setFailedText('Failed to create cache directory') + const taskFailedString = { + text: 'Failed to create cache directory', + key: 'MessageTaskFailedToCreateCacheDirectory' + } + task.setFailed(taskFailedString) this.handleTaskFinished(task) return } @@ -168,7 +185,11 @@ class AudioMetadataMangaer { const success = await ffmpegHelpers.writeFFMetadataFile(task.data.metadataObject, task.data.chapters, ffmetadataPath) if (!success) { Logger.error(`[AudioMetadataManager] Failed to write ffmetadata file for audiobook "${task.data.libraryItemId}"`) - task.setFailedText('Failed to write metadata file.') + const taskFailedString = { + text: 'Failed to write metadata file', + key: 'MessageTaskFailedToWriteMetadataFile' + } + task.setFailed(taskFailedString) this.handleTaskFinished(task) return } @@ -190,7 +211,12 @@ class AudioMetadataMangaer { Logger.debug(`[AudioMetadataManager] Backed up audio file at "${backupFilePath}"`) } catch (err) { Logger.error(`[AudioMetadataManager] Failed to backup audio file "${af.path}"`, err) - task.setFailedText(`Failed to backup audio file "${Path.basename(af.path)}"`) + const taskFailedString = { + text: `Failed to backup audio file "${Path.basename(af.path)}"`, + key: 'MessageTaskFailedToBackupAudioFile', + subs: [Path.basename(af.path)] + } + task.setFailed(taskFailedString) this.handleTaskFinished(task) return } @@ -204,7 +230,12 @@ class AudioMetadataMangaer { Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${af.path}"`) } catch (err) { Logger.error(`[AudioMetadataManager] Failed to tag audio file "${af.path}"`, err) - task.setFailedText(`Failed to tag audio file "${Path.basename(af.path)}"`) + const taskFailedString = { + text: `Failed to embed metadata in file "${Path.basename(af.path)}"`, + key: 'MessageTaskFailedToEmbedMetadataInFile', + subs: [Path.basename(af.path)] + } + task.setFailed(taskFailedString) this.handleTaskFinished(task) return } diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index 9e0bdbc2..4e6c3fb8 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -127,14 +127,22 @@ class PodcastManager { if (!success) { await fs.remove(this.currentDownload.targetPath) this.currentDownload.setFinished(false) - task.setFailedText('Failed to download episode') + const taskFailedString = { + text: 'Failed', + key: 'MessageTaskFailed' + } + task.setFailed(taskFailedString) } else { Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`) this.currentDownload.setFinished(true) task.setFinished() } } else { - task.setFailedText('Failed to download episode') + const taskFailedString = { + text: 'Failed', + key: 'MessageTaskFailed' + } + task.setFailed(taskFailedString) this.currentDownload.setFinished(false) } @@ -560,7 +568,12 @@ class PodcastManager { numPodcastsAdded++ } - task.setFinished(`Added ${numPodcastsAdded} podcasts`) + const taskFinishedString = { + text: `Added ${numPodcastsAdded} podcasts`, + key: 'MessageTaskOpmlImportFinished', + subs: [numPodcastsAdded] + } + task.setFinished(taskFinishedString) TaskManager.taskFinished(task) Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Finished OPML import. Created ${numPodcastsAdded} podcasts out of ${rssFeedUrls.length} RSS feed URLs`) } diff --git a/server/managers/TaskManager.js b/server/managers/TaskManager.js index 52c093a9..5067f841 100644 --- a/server/managers/TaskManager.js +++ b/server/managers/TaskManager.js @@ -63,7 +63,7 @@ class TaskManager { createAndEmitFailedTask(action, titleString, descriptionString, errorMessageString) { const task = new Task() task.setData(action, titleString, descriptionString, false) - task.setFailedText(errorMessageString) + task.setFailed(errorMessageString) SocketAuthority.emitter('task_started', task.toJSON()) return task } diff --git a/server/objects/Task.js b/server/objects/Task.js index 0409cad6..e6fb3963 100644 --- a/server/objects/Task.js +++ b/server/objects/Task.js @@ -57,8 +57,14 @@ class Task { action: this.action, data: this.data ? { ...this.data } : {}, title: this.title, + titleKey: this.titleKey, + titleSubs: this.titleSubs, description: this.description, + descriptionKey: this.descriptionKey, + descriptionSubs: this.descriptionSubs, error: this.error, + errorKey: this.errorKey, + errorSubs: this.errorSubs, showSuccess: this.showSuccess, isFailed: this.isFailed, isFinished: this.isFinished, @@ -104,30 +110,19 @@ class Task { this.setFinished() } - /** - * Set task as failed without translation key - * TODO: Remove this method after all tasks are using translation keys - * - * @param {string} message - */ - setFailedText(message) { - this.error = message - this.errorKey = null - this.errorSubs = null - this.isFailed = true - this.failedAt = Date.now() - this.setFinished() - } - /** * Set task as finished - * TODO: Update to use translation keys * - * @param {string} [newDescription] update description + * @param {TaskString} [newDescriptionString] update description + * @param {boolean} [clearDescription] clear description */ - setFinished(newDescription = null) { - if (newDescription) { - this.description = newDescription + setFinished(newDescriptionString = null, clearDescription = false) { + if (newDescriptionString) { + this.description = newDescriptionString.text + this.descriptionKey = newDescriptionString.key || null + this.descriptionSubs = newDescriptionString.subs || null + } else if (clearDescription) { + this.description = null this.descriptionKey = null this.descriptionSubs = null } diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index 8994aa23..220c6eb4 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -18,7 +18,6 @@ class LibraryScan { this.startedAt = null this.finishedAt = null this.elapsed = null - this.error = null this.resultsMissing = 0 this.resultsAdded = 0 @@ -55,22 +54,6 @@ class LibraryScan { get elapsedTimestamp() { return secondsToTimestamp(this.elapsed / 1000) } - get getScanEmitData() { - return { - id: this.libraryId, - type: this.type, - name: this.libraryName, - error: this.error, - results: { - added: this.resultsAdded, - updated: this.resultsUpdated, - missing: this.resultsMissing - } - } - } - get totalResults() { - return this.resultsAdded + this.resultsUpdated + this.resultsMissing - } get logFilename() { return date.format(new Date(), 'YYYY-MM-DD') + '_' + this.id + '.txt' } @@ -79,10 +62,19 @@ class LibraryScan { if (this.resultsAdded) strs.push(`${this.resultsAdded} added`) if (this.resultsUpdated) strs.push(`${this.resultsUpdated} updated`) if (this.resultsMissing) strs.push(`${this.resultsMissing} missing`) - const changesDetected = strs.length > 0 ? strs.join(', ') : 'No changes detected' + const changesDetected = strs.length > 0 ? strs.join(', ') : 'No changes needed' const timeElapsed = `(${elapsedPretty(this.elapsed / 1000)})` - const error = this.error ? `${this.error}. ` : '' - return `${error}${changesDetected} ${timeElapsed}` + return `${changesDetected} ${timeElapsed}` + } + + get scanResults() { + return { + added: this.resultsAdded, + updated: this.resultsUpdated, + missing: this.resultsMissing, + elapsed: this.elapsed, + text: this.scanResultsString + } } toJSON() { @@ -93,7 +85,6 @@ class LibraryScan { startedAt: this.startedAt, finishedAt: this.finishedAt, elapsed: this.elapsed, - error: this.error, resultsAdded: this.resultsAdded, resultsUpdated: this.resultsUpdated, resultsMissing: this.resultsMissing @@ -113,14 +104,9 @@ class LibraryScan { this.startedAt = Date.now() } - /** - * - * @param {string} error - */ - setComplete(error = null) { + setComplete() { this.finishedAt = Date.now() this.elapsed = this.finishedAt - this.startedAt - this.error = error } getLogLevelString(level) { diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 6b9f7893..b8fcd99e 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -18,6 +18,7 @@ const Task = require('../objects/Task') class LibraryScanner { constructor() { this.cancelLibraryScan = {} + /** @type {string[]} - library ids */ this.librariesScanning = [] this.scanningFilesChanged = false @@ -30,7 +31,7 @@ class LibraryScanner { * @returns {boolean} */ isLibraryScanning(libraryId) { - return this.librariesScanning.some((ls) => ls.id === libraryId) + return this.librariesScanning.some((lid) => lid === libraryId) } /** @@ -38,8 +39,7 @@ class LibraryScanner { * @param {string} libraryId */ setCancelLibraryScan(libraryId) { - const libraryScanning = this.librariesScanning.find((ls) => ls.id === libraryId) - if (!libraryScanning) return + if (!this.isLibraryScanning(libraryId)) return this.cancelLibraryScan[libraryId] = true } @@ -69,7 +69,7 @@ class LibraryScanner { const libraryScan = new LibraryScan() libraryScan.setData(library) libraryScan.verbose = true - this.librariesScanning.push(libraryScan.getScanEmitData) + this.librariesScanning.push(libraryScan.libraryId) const taskData = { libraryId: library.id, @@ -103,17 +103,31 @@ class LibraryScanner { await library.save() } - task.setFinished(`${canceled ? 'Canceled' : 'Completed'}. ${libraryScan.scanResultsString}`) + task.data.scanResults = libraryScan.scanResults + if (canceled) { + const taskFinishedString = { + text: 'Task canceled by user', + key: 'MessageTaskCanceledByUser' + } + task.setFinished(taskFinishedString) + } else { + task.setFinished(null, true) + } } catch (err) { - libraryScan.setComplete(err) + libraryScan.setComplete() Logger.error(`[LibraryScanner] Library scan ${libraryScan.id} failed after ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}.`, err) - task.setFailedText(`Failed. ${libraryScan.scanResultsString}`) + task.data.scanResults = libraryScan.scanResults + const taskFailedString = { + text: 'Failed', + key: 'MessageTaskFailed' + } + task.setFailed(taskFailedString) } if (this.cancelLibraryScan[libraryScan.libraryId]) delete this.cancelLibraryScan[libraryScan.libraryId] - this.librariesScanning = this.librariesScanning.filter((ls) => ls.id !== library.id) + this.librariesScanning = this.librariesScanning.filter((lid) => lid !== library.id) TaskManager.taskFinished(task) @@ -446,9 +460,15 @@ class LibraryScanner { if (results.added) resultStrs.push(`${results.added} added`) if (results.updated) resultStrs.push(`${results.updated} updated`) if (results.removed) resultStrs.push(`${results.removed} missing`) - let scanResultStr = 'Scan finished with no changes' + let scanResultStr = 'No changes needed' if (resultStrs.length) scanResultStr = resultStrs.join(', ') - pendingTask.setFinished(scanResultStr) + + pendingTask.data.scanResults = { + ...results, + text: scanResultStr, + elapsed: Date.now() - pendingTask.startedAt + } + pendingTask.setFinished(null, true) TaskManager.taskFinished(pendingTask) this.scanningFilesChanged = false diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 6bb62706..cfdeb140 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -364,7 +364,7 @@ class Scanner { const libraryScan = new LibraryScan() libraryScan.setData(library, 'match') - LibraryScanner.librariesScanning.push(libraryScan.getScanEmitData) + LibraryScanner.librariesScanning.push(libraryScan.libraryId) const taskData = { libraryId: library.id } @@ -397,15 +397,29 @@ class Scanner { if (offset === 0) { Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) - libraryScan.setComplete('Library has no items') - task.setFailedText(libraryScan.error) + libraryScan.setComplete() + const taskFailedString = { + text: 'No items found', + key: 'MessageNoItemsFound' + } + task.setFailed(taskFailedString) } else { libraryScan.setComplete() - task.setFinished(isCanceled ? 'Canceled' : libraryScan.scanResultsString) + + task.data.scanResults = libraryScan.scanResults + if (isCanceled) { + const taskFinishedString = { + text: 'Task canceled by user', + key: 'MessageTaskCanceledByUser' + } + task.setFinished(taskFinishedString) + } else { + task.setFinished(null, true) + } } delete LibraryScanner.cancelLibraryScan[libraryScan.libraryId] - LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter((ls) => ls.id !== library.id) + LibraryScanner.librariesScanning = LibraryScanner.librariesScanning.filter((lid) => lid !== library.id) TaskManager.taskFinished(task) } }