From 8512d5e693f5fdde3c9aad0d074526301498d7da Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 20 Sep 2024 17:18:29 -0500 Subject: [PATCH 01/72] Update Task object to handle translation keys with subs --- server/Watcher.js | 7 ++- server/managers/AbMergeManager.js | 49 +++++++++++++--- server/managers/AudioMetadataManager.js | 25 +++++--- server/managers/PodcastManager.js | 72 ++++++++++++++++++++--- server/managers/TaskManager.js | 27 +++++---- server/objects/Task.js | 76 ++++++++++++++++++++----- server/scanner/LibraryScanner.js | 9 ++- server/scanner/Scanner.js | 9 ++- 8 files changed, 220 insertions(+), 54 deletions(-) diff --git a/server/Watcher.js b/server/Watcher.js index 83c45234c..0a5867bd6 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 d94e94898..1fed95a18 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 2dcbb1d44..8cd8039c8 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 adec59871..9e0bdbc2d 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 1a8b6c85b..52c093a92 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 db7e490e6..0409cad62 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(message) { + 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 + */ + 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 5cd8b5c62..6b9f7893e 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 06657de22..6bb62706d 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) From 1dec8ae12289638ebbc3d9c763071f7804b92935 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 21 Sep 2024 14:02:57 -0500 Subject: [PATCH 02/72] 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 14972df98..12d1b6183 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 2eb6b123c..0ec5cccee 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 160ff9439..ad08ebf6a 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 9e1643e17..6da92f6f6 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 0a5867bd6..0e34fc66b 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 1fed95a18..ea70d73c7 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 8cd8039c8..7911178e3 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 9e0bdbc2d..4e6c3fb83 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 52c093a92..5067f841a 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 0409cad62..e6fb39636 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 8994aa231..220c6eb4a 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 6b9f7893e..b8fcd99e1 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 6bb62706d..cfdeb1402 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) } } From decde230aa78b1838a91645229e3b189e0f1b1cd Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 22 Sep 2024 14:15:17 -0500 Subject: [PATCH 03/72] Update:Some logs to include library item id #3440 --- server/controllers/LibraryItemController.js | 2 +- server/scanner/LibraryItemScanner.js | 45 +++++++++++---------- server/scanner/LibraryScanner.js | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index c77e1d3a5..6550b9e93 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -480,7 +480,7 @@ class LibraryItemController { const libraryId = itemsToDelete[0].libraryId for (const libraryItem of itemsToDelete) { const libraryItemPath = libraryItem.path - Logger.info(`[LibraryItemController] Deleting Library Item "${libraryItem.media.metadata.title}"`) + Logger.info(`[LibraryItemController] Deleting Library Item "${libraryItem.media.metadata.title}" with id "${libraryItem.id}"`) const mediaItemIds = libraryItem.mediaType === 'podcast' ? libraryItem.media.episodes.map((ep) => ep.id) : [libraryItem.media.id] await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds) if (hardDelete) { diff --git a/server/scanner/LibraryItemScanner.js b/server/scanner/LibraryItemScanner.js index 1c3123df1..38608e479 100644 --- a/server/scanner/LibraryItemScanner.js +++ b/server/scanner/LibraryItemScanner.js @@ -15,12 +15,12 @@ const LibraryFile = require('../objects/files/LibraryFile') const SocketAuthority = require('../SocketAuthority') class LibraryItemScanner { - constructor() { } + constructor() {} /** * Scan single library item - * - * @param {string} libraryItemId + * + * @param {string} libraryItemId * @param {{relPath:string, path:string}} [updateLibraryItemDetails] used by watcher when item folder was renamed * @returns {number} ScanResult */ @@ -76,8 +76,8 @@ class LibraryItemScanner { /** * Remove empty authors and series - * @param {string} libraryId - * @param {ScanLogger} scanLogger + * @param {string} libraryId + * @param {ScanLogger} scanLogger * @returns {Promise} */ async checkAuthorsAndSeriesRemovedFromBooks(libraryId, scanLogger) { @@ -90,11 +90,11 @@ class LibraryItemScanner { } /** - * - * @param {string} libraryItemPath - * @param {import('../models/Library')} library - * @param {import('../models/LibraryFolder')} folder - * @param {boolean} isSingleMediaItem + * + * @param {string} libraryItemPath + * @param {import('../models/Library')} library + * @param {import('../models/LibraryFolder')} folder + * @param {boolean} isSingleMediaItem * @returns {Promise} */ async getLibraryItemScanData(libraryItemPath, library, folder, isSingleMediaItem) { @@ -105,7 +105,8 @@ class LibraryItemScanner { let fileItems = [] - if (isSingleMediaItem) { // Single media item in root of folder + if (isSingleMediaItem) { + // Single media item in root of folder fileItems = [ { fullpath: libraryItemPath, @@ -151,9 +152,9 @@ class LibraryItemScanner { } /** - * - * @param {import('../models/LibraryItem')} existingLibraryItem - * @param {LibraryItemScanData} libraryItemData + * + * @param {import('../models/LibraryItem')} existingLibraryItem + * @param {LibraryItemScanData} libraryItemData * @param {import('../models/Library').LibrarySettingsObject} librarySettings * @param {LibraryScan} libraryScan * @returns {Promise<{libraryItem:LibraryItem, wasUpdated:boolean}>} @@ -167,8 +168,8 @@ class LibraryItemScanner { } /** - * - * @param {LibraryItemScanData} libraryItemData + * + * @param {LibraryItemScanData} libraryItemData * @param {import('../models/Library').LibrarySettingsObject} librarySettings * @param {LibraryScan} libraryScan * @returns {Promise} @@ -181,17 +182,17 @@ class LibraryItemScanner { newLibraryItem = await PodcastScanner.scanNewPodcastLibraryItem(libraryItemData, librarySettings, libraryScan) } if (newLibraryItem) { - libraryScan.addLog(LogLevel.INFO, `Created new library item "${newLibraryItem.relPath}"`) + libraryScan.addLog(LogLevel.INFO, `Created new library item "${newLibraryItem.relPath}" with id "${newLibraryItem.id}"`) } return newLibraryItem } /** * Scan library item folder coming from Watcher - * @param {string} libraryItemPath - * @param {import('../models/Library')} library - * @param {import('../models/LibraryFolder')} folder - * @param {boolean} isSingleMediaItem + * @param {string} libraryItemPath + * @param {import('../models/Library')} library + * @param {import('../models/LibraryFolder')} folder + * @param {boolean} isSingleMediaItem * @returns {Promise} ScanResult */ async scanPotentialNewLibraryItem(libraryItemPath, library, folder, isSingleMediaItem) { @@ -204,4 +205,4 @@ class LibraryItemScanner { return this.scanNewLibraryItem(libraryItemScanData, library.settings, scanLogger) } } -module.exports = new LibraryItemScanner() \ No newline at end of file +module.exports = new LibraryItemScanner() diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index b8fcd99e1..bd0bb310f 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -618,7 +618,7 @@ class LibraryScanner { } } // Scan library item for updates - Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) + Logger.debug(`[LibraryScanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" with id "${existingLibraryItem.id}" - scan for updates`) itemGroupingResults[itemDir] = await LibraryItemScanner.scanLibraryItem(existingLibraryItem.id, updatedLibraryItemDetails) continue } else if (library.settings.audiobooksOnly && !hasAudioFiles(fileUpdateGroup, itemDir)) { From 5b22e945dab1d31ecb7727d26e2456f75151d1d0 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 23 Sep 2024 16:36:56 -0500 Subject: [PATCH 04/72] Update:Format numbers on user listening stats chart #3441 --- client/components/stats/DailyListeningChart.vue | 8 ++++---- server/controllers/LibraryItemController.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/components/stats/DailyListeningChart.vue b/client/components/stats/DailyListeningChart.vue index d96813243..ca2cd714b 100644 --- a/client/components/stats/DailyListeningChart.vue +++ b/client/components/stats/DailyListeningChart.vue @@ -35,22 +35,22 @@

{{ $strings.LabelStatsWeekListening }}

-

{{ totalMinutesListeningThisWeek }}

+

{{ $formatNumber(totalMinutesListeningThisWeek) }}

{{ $strings.LabelStatsMinutes }}

{{ $strings.LabelStatsDailyAverage }}

-

{{ averageMinutesPerDay }}

+

{{ $formatNumber(averageMinutesPerDay) }}

{{ $strings.LabelStatsMinutes }}

{{ $strings.LabelStatsBestDay }}

-

{{ mostListenedDay }}

+

{{ $formatNumber(mostListenedDay) }}

{{ $strings.LabelStatsMinutes }}

{{ $strings.LabelStatsDays }}

-

{{ daysInARow }}

+

{{ $formatNumber(daysInARow) }}

{{ $strings.LabelStatsInARow }}

diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 6550b9e93..fe8539bc3 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -480,7 +480,7 @@ class LibraryItemController { const libraryId = itemsToDelete[0].libraryId for (const libraryItem of itemsToDelete) { const libraryItemPath = libraryItem.path - Logger.info(`[LibraryItemController] Deleting Library Item "${libraryItem.media.metadata.title}" with id "${libraryItem.id}"`) + Logger.info(`[LibraryItemController] (${hardDelete ? 'Hard' : 'Soft'}) deleting Library Item "${libraryItem.media.metadata.title}" with id "${libraryItem.id}"`) const mediaItemIds = libraryItem.mediaType === 'podcast' ? libraryItem.media.episodes.map((ep) => ep.id) : [libraryItem.media.id] await this.handleDeleteLibraryItem(libraryItem.mediaType, libraryItem.id, mediaItemIds) if (hardDelete) { From bb7938f66d816709635fb132faa85d23479e922a Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 24 Sep 2024 10:54:25 -0500 Subject: [PATCH 05/72] Update:When merging embedded chapters from multiple files filter out ~0 duration chapters #3361 --- server/scanner/AudioFileScanner.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js index 1cd148f6f..2a70e6a07 100644 --- a/server/scanner/AudioFileScanner.js +++ b/server/scanner/AudioFileScanner.js @@ -475,16 +475,26 @@ class AudioFileScanner { audioFiles.forEach((file) => { if (file.duration) { - const afChapters = - file.chapters?.map((c) => ({ - ...c, - id: c.id + currChapterId, - start: c.start + currStartTime, - end: c.end + currStartTime - })) ?? [] + // Multi-file audiobook may include the previous and next chapters embedded with close to 0 duration + // Filter these out and log a warning + // See https://github.com/advplyr/audiobookshelf/issues/3361 + const afChaptersCleaned = + file.chapters?.filter((c) => { + if (c.end - c.start < 0.1) { + libraryScan.addLog(LogLevel.WARN, `Chapter "${c.title}" has invalid duration of ${c.end - c.start} seconds. Skipping this chapter.`) + return false + } + return true + }) || [] + const afChapters = afChaptersCleaned.map((c) => ({ + ...c, + id: c.id + currChapterId, + start: c.start + currStartTime, + end: c.end + currStartTime + })) chapters = chapters.concat(afChapters) - currChapterId += file.chapters?.length ?? 0 + currChapterId += afChaptersCleaned.length ?? 0 currStartTime += file.duration } }) From 0d31d20f0f335cc7fde10ee938c2cae5a8f7ecc9 Mon Sep 17 00:00:00 2001 From: Greg Lorenzen Date: Tue, 24 Sep 2024 23:00:19 +0000 Subject: [PATCH 06/72] Center align player chapter title --- client/components/player/PlayerUi.vue | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index 968b0ea1f..c3bb4cdac 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -48,15 +48,19 @@ -
-

00:00:00

- -
-

- {{ currentChapterName }}  ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }}) -

-
-

{{ timeRemainingPretty }}

+
+
+

00:00:00

+ +
+
+

+ {{ currentChapterName }}  ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }}) +

+
+
+

{{ timeRemainingPretty }}

+
From b2d41f05839c0b12cedefc305239e760c217ab14 Mon Sep 17 00:00:00 2001 From: Greg Lorenzen Date: Tue, 24 Sep 2024 23:17:26 +0000 Subject: [PATCH 07/72] Move playback speed control next to player volume control --- .../player/PlayerPlaybackControls.vue | 83 +++++++------------ client/components/player/PlayerUi.vue | 8 ++ 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index 39ce4f3c4..1b96a24b2 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -1,38 +1,37 @@ @@ -41,7 +40,6 @@ export default { props: { loading: Boolean, seekLoading: Boolean, - playbackRate: Number, paused: Boolean, hasNextChapter: Boolean, hasNextItemInQueue: Boolean @@ -50,14 +48,6 @@ export default { return {} }, computed: { - playbackRateInput: { - get() { - return this.playbackRate - }, - set(val) { - this.$emit('update:playbackRate', val) - } - }, jumpForwardText() { return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward) }, @@ -89,15 +79,6 @@ export default { jumpForward() { this.$emit('jumpForward') }, - playbackRateUpdated(playbackRate) { - this.$emit('setPlaybackRate', playbackRate) - }, - playbackRateChanged(playbackRate) { - this.$emit('setPlaybackRate', playbackRate) - this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => { - console.error('Failed to update settings', err) - }) - }, getJumpText(setting, prefix) { const amount = this.$store.getters['user/getUserSetting'](setting) if (!amount) return prefix diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index c3bb4cdac..e341da4a3 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -4,6 +4,8 @@
+ + @@ -228,6 +230,12 @@ export default { this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1)) this.setPlaybackRate(this.playbackRate) }, + playbackRateChanged(playbackRate) { + this.setPlaybackRate(playbackRate) + this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => { + console.error('Failed to update settings', err) + }) + }, setPlaybackRate(playbackRate) { this.$emit('setPlaybackRate', playbackRate) }, From 0bc58c254f5ae1df66811b66b682eceb6489f236 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 25 Sep 2024 16:49:24 -0500 Subject: [PATCH 08/72] Update playback speed to no longer use font-mono, adjust position of popup --- .../components/controls/PlaybackSpeedControl.vue | 16 ++++++++-------- client/components/player/PlayerUi.vue | 4 +--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/client/components/controls/PlaybackSpeedControl.vue b/client/components/controls/PlaybackSpeedControl.vue index c4cc29010..9e9f0d54c 100644 --- a/client/components/controls/PlaybackSpeedControl.vue +++ b/client/components/controls/PlaybackSpeedControl.vue @@ -1,9 +1,9 @@ -