diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 043faaca..702da5ba 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -187,6 +187,11 @@ export default { this.$toast.error('Failed to start scan') }) }, + userUpdated(user) { + if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) { + this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening) + } + }, libraryItemAdded(libraryItem) { console.log('libraryItem added', libraryItem) // TODO: Check if libraryItem would be on this shelf @@ -244,23 +249,16 @@ export default { this.libraryItemUpdated(li) }) }, - seriesUpdated(series) { - if (series.hideFromHome) { - this.shelves.forEach((shelf) => { - if (shelf.type == 'book' && shelf.id == 'continue-series') { - // Filter out series books from continue series shelf - shelf.entities = shelf.entities.filter((ent) => { - if (ent.media.metadata.series && ent.media.metadata.series.id == series.id) return false - return true - }) - } else if (shelf.type == 'series') { - // Filter out series from series shelf - shelf.entities = shelf.entities.filter((ent) => { - return ent.id != series.id - }) - } - }) - } + removeAllSeriesFromContinueSeries(seriesIds) { + this.shelves.forEach((shelf) => { + if (shelf.type == 'book' && shelf.id == 'continue-series') { + // Filter out series books from continue series shelf + shelf.entities = shelf.entities.filter((ent) => { + if (ent.media.metadata.series && seriesIds.includes(ent.media.metadata.series.id)) return false + return true + }) + } + }) }, authorUpdated(author) { this.shelves.forEach((shelf) => { @@ -288,7 +286,7 @@ export default { this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated }) if (this.$root.socket) { - this.$root.socket.on('series_updated', this.seriesUpdated) + this.$root.socket.on('user_updated', this.userUpdated) this.$root.socket.on('author_updated', this.authorUpdated) this.$root.socket.on('author_removed', this.authorRemoved) this.$root.socket.on('item_updated', this.libraryItemUpdated) @@ -304,7 +302,7 @@ export default { this.$store.commit('user/removeSettingsListener', 'bookshelf') if (this.$root.socket) { - this.$root.socket.off('series_updated', this.seriesUpdated) + this.$root.socket.off('user_updated', this.userUpdated) this.$root.socket.off('author_updated', this.authorUpdated) this.$root.socket.off('author_removed', this.authorRemoved) this.$root.socket.off('item_updated', this.libraryItemUpdated) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 1e41ce14..bbb34ddc 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -28,7 +28,6 @@ {{ numShowing }}
- Show on Home
@@ -95,8 +94,7 @@ export default { keywordTimeout: null, processingSeries: false, processingIssues: false, - processingAuthors: false, - processingSeriesHideFromHome: false + processingAuthors: false } }, computed: { @@ -152,9 +150,6 @@ export default { seriesName() { return this.selectedSeries ? this.selectedSeries.name : null }, - seriesHideFromHome() { - return this.selectedSeries ? this.selectedSeries.hideFromHome : null - }, seriesProgress() { return this.selectedSeries ? this.selectedSeries.progress : null }, @@ -176,23 +171,6 @@ export default { } }, methods: { - async showSeriesOnHome() { - this.processingSeriesHideFromHome = true - const seriesId = this.selectedSeries.id - this.$axios - .$patch(`/api/series/${seriesId}`, { hideFromHome: false }) - .then((data) => { - console.log('Updated series', data) - this.$toast.success('Series updated successfully') - }) - .catch((error) => { - console.error('Failed to update series', error) - this.$toast.error('Failed to update series') - }) - .finally(() => { - this.processingSeriesHideFromHome = false - }) - }, async matchAllAuthors() { this.processingAuthors = true diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index fe304a0f..c7f029df 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -411,10 +411,10 @@ export default { text: 'Re-Scan' }) } - if (this.userIsAdminOrUp && this.series && this.bookMount) { + if (this.series && this.bookMount) { items.push({ - func: 'hideSeriesFromHome', - text: 'Hide Series from Home' + func: 'hideSeriesFromContinueListening', + text: 'Hide Series from Continue Series' }) } return items @@ -595,17 +595,17 @@ export default { // More menu func this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' }) }, - hideSeriesFromHome() { + hideSeriesFromContinueListening() { const axios = this.$axios || this.$nuxt.$axios this.processing = true axios - .$patch(`/api/series/${this.series.id}`, { hideFromHome: true }) + .$post(`/api/me/series/${this.series.id}/hide`) .then((data) => { - console.log('Series updated', data) + console.log('User updated', data) }) .catch((error) => { console.error('Failed to hide series from home', error) - this.$toast.error('Failed to update series') + this.$toast.error('Failed to update user') }) .finally(() => { this.processing = false diff --git a/server/Server.js b/server/Server.js index 0df379ee..44038d55 100644 --- a/server/Server.js +++ b/server/Server.js @@ -145,7 +145,7 @@ class Server { await this.auth.initTokenSecret() } - await this.checkUserMediaProgress() // Remove invalid user item progress + await this.cleanUserData() // Remove invalid user item progress await this.purgeMetadata() // Remove metadata folders without library item await this.playbackSessionManager.removeInvalidSessions() await this.cacheManager.ensureCachePaths() @@ -368,21 +368,37 @@ class Server { return purged } - // Remove user media progress entries that dont have a library item - // TODO: Check podcast episode exists still - async checkUserMediaProgress() { + // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist + async cleanUserData() { for (let i = 0; i < this.db.users.length; i++) { var _user = this.db.users[i] - if (_user.mediaProgress) { - var itemProgressIdsToRemove = _user.mediaProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId)) - if (itemProgressIdsToRemove.length) { - Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} media progress data to remove from user ${_user.username}`) - for (const lipId of itemProgressIdsToRemove) { - _user.removeMediaProgress(lipId) - } - await this.db.updateEntity('user', _user) + var hasUpdated = false + if (_user.mediaProgress.length) { + const lengthBefore = _user.mediaProgress.length + _user.mediaProgress = _user.mediaProgress.filter(mp => { + const libraryItem = this.db.libraryItems.find(li => li.id === mp.libraryItemId) + if (!libraryItem) return false + if (mp.episodeId && (libraryItem.mediaType !== 'podcast' || !libraryItem.media.checkHasEpisode(mp.episodeId))) return false // Episode not found + return true + }) + + if (lengthBefore > _user.mediaProgress.length) { + Logger.debug(`[Server] Removing ${_user.mediaProgress.length - lengthBefore} media progress data from user ${_user.username}`) + hasUpdated = true } } + if (_user.seriesHideFromContinueListening.length) { + _user.seriesHideFromContinueListening = _user.seriesHideFromContinueListening.filter(seriesId => { + if (!this.db.series.some(se => se.id === seriesId)) { // Series removed + hasUpdated = true + return false + } + return true + }) + } + if (hasUpdated) { + await this.db.updateEntity('user', _user) + } } } diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index d9aca2e9..ffd045d8 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -276,5 +276,21 @@ class MeController { libraryItems: itemsInProgress }) } + + // GET: api/me/series/:id/hide + async hideSeriesFromContinueListening(req, res) { + const series = this.db.series.find(se => se.id === req.params.id) + if (!series) { + Logger.error(`[MeController] hideSeriesFromContinueListening: Series ${req.params.id} not found`) + return res.sendStatus(404) + } + + const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id) + if (hasUpdated) { + await this.db.updateEntity('user', req.user) + this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + } + res.json(req.user.toJSONForBrowser()) + } } module.exports = new MeController() \ No newline at end of file diff --git a/server/objects/entities/Series.js b/server/objects/entities/Series.js index feafc518..0f74c503 100644 --- a/server/objects/entities/Series.js +++ b/server/objects/entities/Series.js @@ -5,7 +5,6 @@ class Series { this.id = null this.name = null this.description = null - this.hideFromHome = false this.addedAt = null this.updatedAt = null @@ -18,7 +17,6 @@ class Series { this.id = series.id this.name = series.name this.description = series.description || null - this.hideFromHome = !!series.hideFromHome this.addedAt = series.addedAt this.updatedAt = series.updatedAt } @@ -28,7 +26,6 @@ class Series { id: this.id, name: this.name, description: this.description, - hideFromHome: this.hideFromHome, addedAt: this.addedAt, updatedAt: this.updatedAt } @@ -46,14 +43,13 @@ class Series { this.id = getId('ser') this.name = data.name this.description = data.description || null - this.hideFromHome = !!data.hideFromHome this.addedAt = Date.now() this.updatedAt = Date.now() } update(series) { if (!series) return false - const keysToUpdate = ['name', 'description', 'hideFromHome'] + const keysToUpdate = ['name', 'description'] var hasUpdated = false for (const key of keysToUpdate) { if (series[key] !== undefined && series[key] !== this[key]) { diff --git a/server/objects/user/User.js b/server/objects/user/User.js index c33fb3d0..220d6ce4 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -15,6 +15,7 @@ class User { this.createdAt = null this.mediaProgress = [] + this.seriesHideFromContinueListening = [] // Series IDs that should not show on home page continue listening this.bookmarks = [] this.settings = {} @@ -92,6 +93,7 @@ class User { type: this.type, token: this.token, mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], + seriesHideFromContinueListening: [...this.seriesHideFromContinueListening], bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, @@ -111,6 +113,7 @@ class User { type: this.type, token: this.token, mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], + seriesHideFromContinueListening: [...this.seriesHideFromContinueListening], bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, @@ -161,6 +164,9 @@ class User { this.bookmarks = user.bookmarks.filter(bm => typeof bm.libraryItemId == 'string').map(bm => new AudioBookmark(bm)) } + this.seriesHideFromContinueListening = [] + if (user.seriesHideFromContinueListening) this.seriesHideFromContinueListening = [...user.seriesHideFromContinueListening] + this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive this.isLocked = user.type === 'root' ? false : !!user.isLocked this.lastSeen = user.lastSeen || null @@ -196,6 +202,13 @@ class User { } }) + if (payload.seriesHideFromContinueListening && Array.isArray(payload.seriesHideFromContinueListening)) { + if (this.seriesHideFromContinueListening.join(',') !== payload.seriesHideFromContinueListening.join(',')) { + hasUpdates = true + this.seriesHideFromContinueListening = [...payload.seriesHideFromContinueListening] + } + } + // And update permissions if (payload.permissions) { for (const key in payload.permissions) { @@ -297,7 +310,13 @@ class User { return wasUpdated } - removeMediaProgress(libraryItemId) { + removeMediaProgress(id) { + if (!this.mediaProgress.some(mp => mp.id === id)) return false + this.mediaProgress = this.mediaProgress.filter(mp => mp.id !== id) + return true + } + + removeMediaProgressForLibraryItem(libraryItemId) { if (!this.mediaProgress.some(lip => lip.libraryItemId == libraryItemId)) return false this.mediaProgress = this.mediaProgress.filter(lip => lip.libraryItemId != libraryItemId) return true @@ -378,5 +397,15 @@ class User { removeBookmark(libraryItemId, time) { this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time)) } + + checkShouldHideSeriesFromContinueListening(seriesId) { + return this.seriesHideFromContinueListening.includes(seriesId) + } + + addSeriesToHideFromContinueListening(seriesId) { + if (this.seriesHideFromContinueListening.includes(seriesId)) return false + this.seriesHideFromContinueListening.push(seriesId) + return true + } } module.exports = User \ No newline at end of file diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 449f8a2d..4853e487 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -150,6 +150,7 @@ class ApiRouter { this.router.patch('/me/settings', MeController.updateSettings.bind(this)) this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this)) this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) + this.router.post('/me/series/:id/hide', MeController.hideSeriesFromContinueListening.bind(this)) // // Backup Routes @@ -304,7 +305,7 @@ class ApiRouter { // Remove libraryItem from users for (let i = 0; i < this.db.users.length; i++) { var user = this.db.users[i] - var madeUpdates = user.removeMediaProgress(libraryItem.id) + var madeUpdates = user.removeMediaProgressForLibraryItem(libraryItem.id) if (madeUpdates) { await this.db.updateEntity('user', user) } diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 72ade464..0a3ebae7 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -415,13 +415,16 @@ module.exports = { const libraryItemJson = libraryItem.toJSONMinified() libraryItemJson.seriesSequence = librarySeries.sequence + const hideFromContinueListening = user.checkShouldHideSeriesFromContinueListening(librarySeries.id) + if (!seriesMap[librarySeries.id]) { const seriesObj = allSeries.find(se => se.id === librarySeries.id) - if (seriesObj && !seriesObj.hideFromHome) { + if (seriesObj) { var series = { ...seriesObj.toJSON(), books: [libraryItemJson], inProgress: bookInProgress, + hideFromContinueListening, bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null, firstBookUnread: bookInProgress ? null : libraryItemJson } @@ -555,7 +558,7 @@ module.exports = { // For Continue Series - Find next book in series for series that are in progress for (const seriesId in seriesMap) { - if (seriesMap[seriesId].inProgress) { + if (seriesMap[seriesId].inProgress && !seriesMap[seriesId].hideFromContinueListening) { seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence) // NEW implementation takes the first book unread with the smallest series sequence