diff --git a/server/Watcher.js b/server/Watcher.js index 83c45234c9..0a5867bd65 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -301,7 +301,12 @@ class FolderWatcher extends EventEmitter { libraryId, libraryName: libwatcher.name } - this.pendingTask = TaskManager.createAndAddTask('watcher-scan', `Scanning file changes in "${libwatcher.name}"`, null, true, taskData) + const taskTitleString = { + text: `Scanning file changes in "${libwatcher.name}"`, + key: 'MessageTaskScanningFileChanges', + subs: [libwatcher.name] + } + this.pendingTask = TaskManager.createAndAddTask('watcher-scan', taskTitleString, null, true, taskData) } this.pendingFileUpdates.push({ path, diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index d94e948987..1fed95a18f 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -40,7 +40,11 @@ class AbMergeManager { * @returns {Promise} */ cancelEncode(task) { - task.setFailed('Task canceled by user') + const taskFailedString = { + text: 'Task canceled by user', + key: 'MessageTaskCanceledByUser' + } + task.setFailed(taskFailedString) return this.removeTask(task, true) } @@ -76,8 +80,17 @@ class AbMergeManager { duration: libraryItem.media.duration, encodeOptions: options } - const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.` - task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData) + + const taskTitleString = { + text: 'Encoding M4b', + key: 'MessageTaskEncodingM4b' + } + const taskDescriptionString = { + text: `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`, + key: 'MessageTaskEncodingM4bDescription', + subs: [libraryItem.media.metadata.title] + } + task.setData('encode-m4b', taskTitleString, taskDescriptionString, false, taskData) TaskManager.addTask(task) Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`) @@ -98,7 +111,11 @@ class AbMergeManager { // Make sure the target directory is writable if (!(await isWritable(task.data.libraryItemDir))) { Logger.error(`[AbMergeManager] Target directory is not writable: ${task.data.libraryItemDir}`) - task.setFailed('Target directory is not writable') + const taskFailedString = { + text: 'Target directory is not writable', + key: 'MessageTaskTargetDirectoryNotWritable' + } + task.setFailed(taskFailedString) this.removeTask(task, true) return } @@ -106,7 +123,11 @@ class AbMergeManager { // Create ffmetadata file if (!(await ffmpegHelpers.writeFFMetadataFile(task.data.ffmetadataObject, task.data.chapters, task.data.ffmetadataPath))) { Logger.error(`[AudioMetadataManager] Failed to write ffmetadata file for audiobook "${task.data.libraryItemId}"`) - task.setFailed('Failed to write metadata file.') + const taskFailedString = { + text: 'Failed to write metadata file', + key: 'MessageTaskFailedToWriteMetadataFile' + } + task.setFailed(taskFailedString) this.removeTask(task, true) return } @@ -137,7 +158,11 @@ class AbMergeManager { Logger.info(`[AbMergeManager] Task cancelled ${task.id}`) } else { Logger.error(`[AbMergeManager] mergeAudioFiles failed`, error) - task.setFailed('Failed to merge audio files') + const taskFailedString = { + text: 'Failed to merge audio files', + key: 'MessageTaskFailedToMergeAudioFiles' + } + task.setFailed(taskFailedString) this.removeTask(task, true) } return @@ -164,7 +189,11 @@ class AbMergeManager { Logger.info(`[AbMergeManager] Task cancelled ${task.id}`) } else { Logger.error(`[AbMergeManager] Failed to write metadata to file "${task.data.tempFilepath}"`) - task.setFailed('Failed to write metadata to m4b file') + const taskFailedString = { + text: 'Failed to write metadata to m4b file', + key: 'MessageTaskFailedToWriteMetadataToM4bFile' + } + task.setFailed(taskFailedString) this.removeTask(task, true) } return @@ -196,7 +225,11 @@ class AbMergeManager { await fs.remove(task.data.tempFilepath) } catch (err) { Logger.error(`[AbMergeManager] Failed to move m4b from ${task.data.tempFilepath} to ${task.data.targetFilepath}`, err) - task.setFailed('Failed to move m4b file') + const taskFailedString = { + text: 'Failed to move m4b file', + key: 'MessageTaskFailedToMoveM4bFile' + } + task.setFailed(taskFailedString) this.removeTask(task, true) return } diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 2dcbb1d445..8cd8039c8a 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -97,8 +97,17 @@ class AudioMetadataMangaer { }, duration: libraryItem.media.duration } - const taskDescription = `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".` - task.setData('embed-metadata', 'Embedding Metadata', taskDescription, false, taskData) + + const taskTitleString = { + text: 'Embedding Metadata', + key: 'MessageTaskEmbeddingMetadata' + } + const taskDescriptionString = { + text: `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`, + key: 'MessageTaskEmbeddingMetadataDescription', + subs: [libraryItem.media.metadata.title] + } + task.setData('embed-metadata', taskTitleString, taskDescriptionString, false, taskData) if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) { Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`) @@ -123,7 +132,7 @@ 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.setFailed('Target directory is not writable') + task.setFailedText('Target directory is not writable') this.handleTaskFinished(task) return } @@ -134,7 +143,7 @@ class AudioMetadataMangaer { await fs.access(af.path, fs.constants.W_OK) } catch (err) { Logger.error(`[AudioMetadataManager] Audio file is not writable: ${af.path}`) - task.setFailed(`Audio file "${Path.basename(af.path)}" is not writable`) + task.setFailedText(`Audio file "${Path.basename(af.path)}" is not writable`) this.handleTaskFinished(task) return } @@ -148,7 +157,7 @@ class AudioMetadataMangaer { cacheDirCreated = true } catch (err) { Logger.error(`[AudioMetadataManager] Failed to create cache directory ${task.data.itemCachePath}`, err) - task.setFailed('Failed to create cache directory') + task.setFailedText('Failed to create cache directory') this.handleTaskFinished(task) return } @@ -159,7 +168,7 @@ 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.setFailed('Failed to write metadata file.') + task.setFailedText('Failed to write metadata file.') this.handleTaskFinished(task) return } @@ -181,7 +190,7 @@ 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.setFailed(`Failed to backup audio file "${Path.basename(af.path)}"`) + task.setFailedText(`Failed to backup audio file "${Path.basename(af.path)}"`) this.handleTaskFinished(task) return } @@ -195,7 +204,7 @@ 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.setFailed(`Failed to tag audio file "${Path.basename(af.path)}"`) + task.setFailedText(`Failed to tag audio file "${Path.basename(af.path)}"`) this.handleTaskFinished(task) return } diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js index adec59871c..9e0bdbc2dc 100644 --- a/server/managers/PodcastManager.js +++ b/server/managers/PodcastManager.js @@ -71,12 +71,20 @@ class PodcastManager { return } - const taskDescription = `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".` const taskData = { libraryId: podcastEpisodeDownload.libraryId, libraryItemId: podcastEpisodeDownload.libraryItemId } - const task = TaskManager.createAndAddTask('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData) + const taskTitleString = { + text: 'Downloading episode', + key: 'MessageDownloadingEpisode' + } + const taskDescriptionString = { + text: `Downloading episode "${podcastEpisodeDownload.podcastEpisode.title}".`, + key: 'MessageTaskDownloadingEpisodeDescription', + subs: [podcastEpisodeDownload.podcastEpisode.title] + } + const task = TaskManager.createAndAddTask('download-podcast-episode', taskTitleString, taskDescriptionString, false, taskData) SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient()) this.currentDownload = podcastEpisodeDownload @@ -119,14 +127,14 @@ class PodcastManager { if (!success) { await fs.remove(this.currentDownload.targetPath) this.currentDownload.setFinished(false) - task.setFailed('Failed to download episode') + task.setFailedText('Failed to download episode') } else { Logger.info(`[PodcastManager] Successfully downloaded podcast episode "${this.currentDownload.podcastEpisode.title}"`) this.currentDownload.setFinished(true) task.setFinished() } } else { - task.setFailed('Failed to download episode') + task.setFailedText('Failed to download episode') this.currentDownload.setFinished(false) } @@ -407,13 +415,35 @@ class PodcastManager { * @param {import('../managers/CronManager')} cronManager */ async createPodcastsFromFeedUrls(rssFeedUrls, folder, autoDownloadEpisodes, cronManager) { - const task = TaskManager.createAndAddTask('opml-import', 'OPML import', `Creating podcasts from ${rssFeedUrls.length} RSS feeds`, true, null) + const taskTitleString = { + text: 'OPML import', + key: 'MessageTaskOpmlImport' + } + const taskDescriptionString = { + text: `Creating podcasts from ${rssFeedUrls.length} RSS feeds`, + key: 'MessageTaskOpmlImportDescription', + subs: [rssFeedUrls.length] + } + const task = TaskManager.createAndAddTask('opml-import', taskTitleString, taskDescriptionString, true, null) let numPodcastsAdded = 0 Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Importing ${rssFeedUrls.length} RSS feeds to folder "${folder.path}"`) for (const feedUrl of rssFeedUrls) { const feed = await getPodcastFeed(feedUrl).catch(() => null) if (!feed?.episodes) { - TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Importing RSS feed "${feedUrl}"`, 'Failed to get podcast feed') + const taskTitleStringFeed = { + text: 'OPML import feed', + key: 'MessageTaskOpmlImportFeed' + } + const taskDescriptionStringFeed = { + text: `Importing RSS feed "${feedUrl}"`, + key: 'MessageTaskOpmlImportFeedDescription', + subs: [feedUrl] + } + const taskErrorString = { + text: 'Failed to get podcast feed', + key: 'MessageTaskOpmlImportFeedFailed' + } + TaskManager.createAndEmitFailedTask('opml-import-feed', taskTitleStringFeed, taskDescriptionStringFeed, taskErrorString) Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Failed to get podcast feed for "${feedUrl}"`) continue } @@ -429,7 +459,20 @@ class PodcastManager { })) > 0 if (existingLibraryItem) { Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Podcast already exists at path "${podcastPath}"`) - TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Creating podcast "${feed.metadata.title}"`, 'Podcast already exists at path') + const taskTitleStringFeed = { + text: 'OPML import feed', + key: 'MessageTaskOpmlImportFeed' + } + const taskDescriptionStringPodcast = { + text: `Creating podcast "${feed.metadata.title}"`, + key: 'MessageTaskOpmlImportFeedPodcastDescription', + subs: [feed.metadata.title] + } + const taskErrorString = { + text: 'Podcast already exists at path', + key: 'MessageTaskOpmlImportFeedPodcastExists' + } + TaskManager.createAndEmitFailedTask('opml-import-feed', taskTitleStringFeed, taskDescriptionStringPodcast, taskErrorString) continue } @@ -442,7 +485,20 @@ class PodcastManager { }) if (!successCreatingPath) { Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Failed to create podcast folder at "${podcastPath}"`) - TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Creating podcast "${feed.metadata.title}"`, 'Failed to create podcast folder') + const taskTitleStringFeed = { + text: 'OPML import feed', + key: 'MessageTaskOpmlImportFeed' + } + const taskDescriptionStringPodcast = { + text: `Creating podcast "${feed.metadata.title}"`, + key: 'MessageTaskOpmlImportFeedPodcastDescription', + subs: [feed.metadata.title] + } + const taskErrorString = { + text: 'Failed to create podcast folder', + key: 'MessageTaskOpmlImportFeedPodcastFailed' + } + TaskManager.createAndEmitFailedTask('opml-import-feed', taskTitleStringFeed, taskDescriptionStringPodcast, taskErrorString) continue } diff --git a/server/managers/TaskManager.js b/server/managers/TaskManager.js index 1a8b6c85b0..52c093a92b 100644 --- a/server/managers/TaskManager.js +++ b/server/managers/TaskManager.js @@ -1,6 +1,13 @@ const SocketAuthority = require('../SocketAuthority') const Task = require('../objects/Task') +/** + * @typedef TaskString + * @property {string} text + * @property {string} key + * @property {string[]} [subs] + */ + class TaskManager { constructor() { /** @type {Task[]} */ @@ -33,14 +40,14 @@ class TaskManager { * Create new task and add * * @param {string} action - * @param {string} title - * @param {string} description + * @param {TaskString} titleString + * @param {TaskString|null} descriptionString * @param {boolean} showSuccess * @param {Object} [data] */ - createAndAddTask(action, title, description, showSuccess, data = {}) { + createAndAddTask(action, titleString, descriptionString, showSuccess, data = {}) { const task = new Task() - task.setData(action, title, description, showSuccess, data) + task.setData(action, titleString, descriptionString, showSuccess, data) this.addTask(task) return task } @@ -49,14 +56,14 @@ class TaskManager { * Create new failed task and add * * @param {string} action - * @param {string} title - * @param {string} description - * @param {string} errorMessage + * @param {TaskString} titleString + * @param {TaskString|null} descriptionString + * @param {TaskString} errorMessageString */ - createAndEmitFailedTask(action, title, description, errorMessage) { + createAndEmitFailedTask(action, titleString, descriptionString, errorMessageString) { const task = new Task() - task.setData(action, title, description, false) - task.setFailed(errorMessage) + task.setData(action, titleString, descriptionString, false) + task.setFailedText(errorMessageString) SocketAuthority.emitter('task_started', task.toJSON()) return task } diff --git a/server/objects/Task.js b/server/objects/Task.js index db7e490e66..0409cad62c 100644 --- a/server/objects/Task.js +++ b/server/objects/Task.js @@ -1,4 +1,11 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 + +/** + * @typedef TaskString + * @property {string} text + * @property {string} key + * @property {string[]} [subs] + */ class Task { constructor() { @@ -11,10 +18,25 @@ class Task { /** @type {string} */ this.title = null + /** @type {string} - Used for translation */ + this.titleKey = null + /** @type {string[]} - Used for translation */ + this.titleSubs = null + /** @type {string} */ this.description = null + /** @type {string} - Used for translation */ + this.descriptionKey = null + /** @type {string[]} - Used for translation */ + this.descriptionSubs = null + /** @type {string} */ this.error = null + /** @type {string} - Used for translation */ + this.errorKey = null + /** @type {string[]} - Used for translation */ + this.errorSubs = null + /** @type {boolean} client should keep the task visible after success */ this.showSuccess = false @@ -47,30 +69,51 @@ class Task { /** * Set initial task data - * - * @param {string} action - * @param {string} title - * @param {string} description - * @param {boolean} showSuccess - * @param {Object} [data] + * + * @param {string} action + * @param {TaskString} titleString + * @param {TaskString|null} descriptionString + * @param {boolean} showSuccess + * @param {Object} [data] */ - setData(action, title, description, showSuccess, data = {}) { + setData(action, titleString, descriptionString, showSuccess, data = {}) { this.id = uuidv4() this.action = action this.data = { ...data } - this.title = title - this.description = description + this.title = titleString.text + this.titleKey = titleString.key || null + this.titleSubs = titleString.subs || null + this.description = descriptionString?.text || null + this.descriptionKey = descriptionString?.key || null + this.descriptionSubs = descriptionString?.subs || null this.showSuccess = showSuccess this.startedAt = Date.now() } /** * Set task as failed - * - * @param {string} message error message + * + * @param {TaskString} messageString + */ + setFailed(messageString) { + this.error = messageString.text + this.errorKey = messageString.key || null + this.errorSubs = messageString.subs || null + this.isFailed = true + this.failedAt = Date.now() + this.setFinished() + } + + /** + * Set task as failed without translation key + * TODO: Remove this method after all tasks are using translation keys + * + * @param {string} message */ - setFailed(message) { + setFailedText(message) { this.error = message + this.errorKey = null + this.errorSubs = null this.isFailed = true this.failedAt = Date.now() this.setFinished() @@ -78,15 +121,18 @@ class Task { /** * Set task as finished - * + * TODO: Update to use translation keys + * * @param {string} [newDescription] update description */ setFinished(newDescription = null) { if (newDescription) { this.description = newDescription + this.descriptionKey = null + this.descriptionSubs = null } this.isFinished = true this.finishedAt = Date.now() } } -module.exports = Task \ No newline at end of file +module.exports = Task diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index 5cd8b5c62d..6b9f7893e1 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -76,7 +76,12 @@ class LibraryScanner { libraryName: library.name, libraryMediaType: library.mediaType } - const task = TaskManager.createAndAddTask('library-scan', `Scanning "${library.name}" library`, null, true, taskData) + const taskTitleString = { + text: `Scanning "${library.name}" library`, + key: 'MessageTaskScanningLibrary', + subs: [library.name] + } + const task = TaskManager.createAndAddTask('library-scan', taskTitleString, null, true, taskData) Logger.info(`[LibraryScanner] Starting${forceRescan ? ' (forced)' : ''} library scan ${libraryScan.id} for ${libraryScan.libraryName}`) @@ -104,7 +109,7 @@ class LibraryScanner { Logger.error(`[LibraryScanner] Library scan ${libraryScan.id} failed after ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}.`, err) - task.setFailed(`Failed. ${libraryScan.scanResultsString}`) + task.setFailedText(`Failed. ${libraryScan.scanResultsString}`) } if (this.cancelLibraryScan[libraryScan.libraryId]) delete this.cancelLibraryScan[libraryScan.libraryId] diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 06657de228..6bb62706d7 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -368,7 +368,12 @@ class Scanner { const taskData = { libraryId: library.id } - const task = TaskManager.createAndAddTask('library-match-all', `Matching books in "${library.name}"`, null, true, taskData) + const taskTitleString = { + text: `Matching books in "${library.name}"`, + key: 'MessageTaskMatchingBooksInLibrary', + subs: [library.name] + } + const task = TaskManager.createAndAddTask('library-match-all', taskTitleString, null, true, taskData) Logger.info(`[Scanner] matchLibraryItems: Starting library match scan ${libraryScan.id} for ${libraryScan.libraryName}`) let hasMoreChunks = true @@ -393,7 +398,7 @@ class Scanner { if (offset === 0) { Logger.error(`[Scanner] matchLibraryItems: Library has no items ${library.id}`) libraryScan.setComplete('Library has no items') - task.setFailed(libraryScan.error) + task.setFailedText(libraryScan.error) } else { libraryScan.setComplete() task.setFinished(isCanceled ? 'Canceled' : libraryScan.scanResultsString)