From 91aea4f754f0363913cbc030079965418d2ff24e Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 24 Oct 2024 17:19:51 -0500 Subject: [PATCH] Add:Library settings for mark as finished when time remaining or percent complete #837 --- .../components/modals/libraries/EditModal.vue | 6 +-- .../modals/libraries/LibrarySettings.vue | 51 ++++++++++++++++++- client/strings/en-us.json | 3 ++ server/controllers/LibraryController.js | 29 ++++++++++- server/managers/PlaybackSessionManager.js | 2 +- server/models/Library.js | 10 +++- server/models/MediaProgress.js | 14 ++--- server/models/User.js | 2 +- 8 files changed, 99 insertions(+), 18 deletions(-) diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 9910a23678..6733b7b867 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -111,7 +111,6 @@ export default { }, updateLibrary(library) { this.mapLibraryToCopy(library) - console.log('Updated library', this.libraryCopy) }, getNewLibraryData() { return { @@ -128,7 +127,9 @@ export default { autoScanCronExpression: null, hideSingleBookSeries: false, onlyShowLaterBooksInContinueSeries: false, - metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] + metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'], + markAsFinishedPercentComplete: null, + markAsFinishedTimeRemaining: 10 } } }, @@ -236,7 +237,6 @@ export default { this.show = false this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name])) if (!this.$store.state.libraries.currentLibraryId) { - console.log('Setting initially library id', res.id) // First library added this.$store.dispatch('libraries/fetch', res.id) } diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index 896a6837a9..d3b40de957 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -75,6 +75,20 @@
+
+
+ +
+
+
+ +
+ +
{{ markAsFinishedWhen === 'timeRemaining' ? '' : '%' }}
+
+
+
+
@@ -99,7 +113,9 @@ export default { epubsAllowScriptedContent: false, hideSingleBookSeries: false, onlyShowLaterBooksInContinueSeries: false, - podcastSearchRegion: 'us' + podcastSearchRegion: 'us', + markAsFinishedWhen: 'timeRemaining', + markAsFinishedValue: 10 } }, computed: { @@ -121,10 +137,34 @@ export default { providers() { if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders return this.$store.state.scanners.providers + }, + maskAsFinishedWhenItems() { + return [ + { + text: this.$strings.LabelSettingsLibraryMarkAsFinishedTimeRemaining, + value: 'timeRemaining' + }, + { + text: this.$strings.LabelSettingsLibraryMarkAsFinishedPercentComplete, + value: 'percentComplete' + } + ] } }, methods: { + markAsFinishedWhenChanged(val) { + if (val === 'percentComplete' && this.markAsFinishedValue > 100) { + this.markAsFinishedValue = 100 + } + this.formUpdated() + }, + markAsFinishedChanged(val) { + this.formUpdated() + }, getLibraryData() { + let markAsFinishedTimeRemaining = this.markAsFinishedWhen === 'timeRemaining' ? Number(this.markAsFinishedValue) : null + let markAsFinishedPercentComplete = this.markAsFinishedWhen === 'percentComplete' ? Number(this.markAsFinishedValue) : null + return { settings: { coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD, @@ -135,7 +175,9 @@ export default { epubsAllowScriptedContent: !!this.epubsAllowScriptedContent, hideSingleBookSeries: !!this.hideSingleBookSeries, onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries, - podcastSearchRegion: this.podcastSearchRegion + podcastSearchRegion: this.podcastSearchRegion, + markAsFinishedTimeRemaining: markAsFinishedTimeRemaining, + markAsFinishedPercentComplete: markAsFinishedPercentComplete } } }, @@ -152,6 +194,11 @@ export default { this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us' + this.markAsFinishedWhen = this.librarySettings.markAsFinishedTimeRemaining ? 'timeRemaining' : 'percentComplete' + if (!this.librarySettings.markAsFinishedTimeRemaining && !this.librarySettings.markAsFinishedPercentComplete) { + this.markAsFinishedWhen = 'timeRemaining' + } + this.markAsFinishedValue = this.librarySettings.markAsFinishedTimeRemaining || this.librarySettings.markAsFinishedPercentComplete || 10 } }, mounted() { diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 301c4ad1ee..918bf68560 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -562,6 +562,9 @@ "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view", + "LabelSettingsLibraryMarkAsFinishedPercentComplete": "Percent complete is greater than", + "LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Time remaining is less than (seconds)", + "LabelSettingsLibraryMarkAsFinishedWhen": "Mark media item as finished when", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsParseSubtitles": "Parse subtitles", diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index bf535bbaa6..82fd34f07d 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -255,15 +255,18 @@ class LibraryController { } // Validate settings + const defaultLibrarySettings = Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType) const updatedSettings = { - ...(req.library.settings || Database.libraryModel.getDefaultLibrarySettingsForMediaType(req.library.mediaType)) + ...(req.library.settings || defaultLibrarySettings) } let hasUpdates = false let hasUpdatedDisableWatcher = false let hasUpdatedScanCron = false if (req.body.settings) { for (const key in req.body.settings) { - if (updatedSettings[key] === undefined) continue + if (!Object.keys(defaultLibrarySettings).includes(key)) { + continue + } if (key === 'metadataPrecedence') { if (!Array.isArray(req.body.settings[key])) { @@ -285,6 +288,28 @@ class LibraryController { updatedSettings[key] = req.body.settings[key] Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`) } + } else if (key === 'markAsFinishedPercentComplete') { + if (req.body.settings[key] !== null && isNaN(req.body.settings[key])) { + return res.status(400).send(`Invalid request. Setting "${key}" must be a number`) + } else if (req.body.settings[key] !== null && (Number(req.body.settings[key]) < 0 || Number(req.body.settings[key]) > 100)) { + return res.status(400).send(`Invalid request. Setting "${key}" must be between 0 and 100`) + } + if (req.body.settings[key] !== updatedSettings[key]) { + hasUpdates = true + updatedSettings[key] = Number(req.body.settings[key]) + Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`) + } + } else if (key === 'markAsFinishedTimeRemaining') { + if (req.body.settings[key] !== null && isNaN(req.body.settings[key])) { + return res.status(400).send(`Invalid request. Setting "${key}" must be a number`) + } else if (req.body.settings[key] !== null && Number(req.body.settings[key]) < 0) { + return res.status(400).send(`Invalid request. Setting "${key}" must be greater than or equal to 0`) + } + if (req.body.settings[key] !== updatedSettings[key]) { + hasUpdates = true + updatedSettings[key] = Number(req.body.settings[key]) + Logger.debug(`[LibraryController] Library "${req.library.name}" updating setting "${key}" to "${updatedSettings[key]}"`) + } } else { if (typeof req.body.settings[key] !== typeof updatedSettings[key]) { return res.status(400).send(`Invalid request. Setting "${key}" must be of type ${typeof updatedSettings[key]}`) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index a26a5c81a2..2ffda937b0 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -349,7 +349,7 @@ class PlaybackSessionManager { progress: session.progress // TODO: Add support for passing in these values from library settings // markAsFinishedTimeRemaining: 5, - // markAsFinishedPercentageComplete: 95 + // markAsFinishedPercentComplete: 95 }) if (updateResponse.mediaProgress) { SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { diff --git a/server/models/Library.js b/server/models/Library.js index 90dd2512ee..36d462cf88 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -12,6 +12,8 @@ const Logger = require('../Logger') * @property {boolean} hideSingleBookSeries Do not show series that only have 1 book * @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read * @property {string[]} metadataPrecedence + * @property {number} markAsFinishedTimeRemaining Time remaining in seconds to mark as finished. (defaults to 10s) + * @property {number} markAsFinishedPercentComplete Percent complete to mark as finished (0-100). If this is set it will be used over markAsFinishedTimeRemaining. */ class Library extends Model { @@ -57,7 +59,9 @@ class Library extends Model { coverAspectRatio: 1, // Square disableWatcher: false, autoScanCronExpression: null, - podcastSearchRegion: 'us' + podcastSearchRegion: 'us', + markAsFinishedPercentComplete: null, + markAsFinishedTimeRemaining: 10 } } else { return { @@ -70,7 +74,9 @@ class Library extends Model { epubsAllowScriptedContent: false, hideSingleBookSeries: false, onlyShowLaterBooksInContinueSeries: false, - metadataPrecedence: this.defaultMetadataPrecedence + metadataPrecedence: this.defaultMetadataPrecedence, + markAsFinishedPercentComplete: null, + markAsFinishedTimeRemaining: 10 } } } diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 052c8f74ef..72c7f7e2be 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -229,18 +229,18 @@ class MediaProgress extends Model { const timeRemaining = this.duration - this.currentTime // Check if progress is far enough to mark as finished - // - If markAsFinishedPercentageComplete is provided, use that otherwise use markAsFinishedTimeRemaining (default 5 seconds) + // - If markAsFinishedPercentComplete is provided, use that otherwise use markAsFinishedTimeRemaining (default 10 seconds) let shouldMarkAsFinished = false if (!this.isFinished && this.duration) { - if (!isNullOrNaN(progressPayload.markAsFinishedPercentageComplete)) { - const markAsFinishedPercentageComplete = Number(progressPayload.markAsFinishedPercentageComplete) / 100 - shouldMarkAsFinished = markAsFinishedPercentageComplete <= this.progress + if (!isNullOrNaN(progressPayload.markAsFinishedPercentComplete)) { + const markAsFinishedPercentComplete = Number(progressPayload.markAsFinishedPercentComplete) / 100 + shouldMarkAsFinished = markAsFinishedPercentComplete < this.progress if (shouldMarkAsFinished) { - Logger.debug(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentageComplete}`) + Logger.debug(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentComplete}`) } } else { - const markAsFinishedTimeRemaining = isNullOrNaN(progressPayload.markAsFinishedTimeRemaining) ? 5 : Number(progressPayload.markAsFinishedTimeRemaining) - shouldMarkAsFinished = timeRemaining <= markAsFinishedTimeRemaining + const markAsFinishedTimeRemaining = isNullOrNaN(progressPayload.markAsFinishedTimeRemaining) ? 10 : Number(progressPayload.markAsFinishedTimeRemaining) + shouldMarkAsFinished = timeRemaining < markAsFinishedTimeRemaining if (shouldMarkAsFinished) { Logger.debug(`[MediaProgress] Marking media progress as finished because time remaining (${timeRemaining}) is less than ${markAsFinishedTimeRemaining} seconds`) } diff --git a/server/models/User.js b/server/models/User.js index aa63aea823..8bd3f742d6 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -28,7 +28,7 @@ const { DataTypes, Model } = sequelize * @property {string} [finishedAt] * @property {number} [lastUpdate] * @property {number} [markAsFinishedTimeRemaining] - * @property {number} [markAsFinishedPercentageComplete] + * @property {number} [markAsFinishedPercentComplete] */ class User extends Model {