From 14a8f844464d599e0deb309e70d12b72bf0923c3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 17 Mar 2022 20:28:04 -0500 Subject: [PATCH] New data model update bookmarks and bookmark routes to use API --- client/components/app/StreamContainer.vue | 10 +-- client/components/modals/BookmarksModal.vue | 38 ++++++--- client/store/user.js | 5 +- server/Server.js | 90 --------------------- server/controllers/MeController.js | 44 ++++++++++ server/objects/user/User.js | 70 ++++++---------- server/routers/ApiRouter.js | 3 + server/utils/dbMigration.js | 39 +++++++-- 8 files changed, 143 insertions(+), 156 deletions(-) diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 6398234f..4eaa0b6e 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -24,7 +24,6 @@
close
- - + @@ -60,7 +59,6 @@ export default { totalDuration: 0, showBookmarksModal: false, bookmarkCurrentTime: 0, - bookmarkAudiobookId: null, playerLoading: false, isPlaying: false, currentTime: 0, @@ -103,9 +101,8 @@ export default { return this.userLibraryItemProgress ? this.userLibraryItemProgress.currentTime || 0 : 0 }, bookmarks() { - return [] - // if (!this.userAudiobook) return [] - // return (this.userAudiobook.bookmarks || []).map((bm) => ({ ...bm })).sort((a, b) => a.time - b.time) + if (!this.libraryItemId) return [] + return this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId) }, streamLibraryItem() { return this.$store.state.streamLibraryItem @@ -215,7 +212,6 @@ export default { } }, showBookmarks() { - this.bookmarkAudiobookId = this.libraryItemId this.bookmarkCurrentTime = this.currentTime this.showBookmarksModal = true }, diff --git a/client/components/modals/BookmarksModal.vue b/client/components/modals/BookmarksModal.vue index 56035b88..273feb0c 100644 --- a/client/components/modals/BookmarksModal.vue +++ b/client/components/modals/BookmarksModal.vue @@ -39,7 +39,7 @@ export default { type: Number, default: 0 }, - audiobookId: String + libraryItemId: String }, data() { return { @@ -76,8 +76,15 @@ export default { this.showBookmarkTitleInput = true }, deleteBookmark(bm) { - var bookmark = { ...bm, audiobookId: this.audiobookId } - this.$root.socket.emit('delete_bookmark', bookmark) + this.$axios + .$delete(`/api/me/item/${this.libraryItemId}/bookmark/${bm.time}`) + .then(() => { + this.$toast.success('Bookmark removed') + }) + .catch((error) => { + this.$toast.error(`Failed to remove bookmark`) + console.error(error) + }) this.show = false }, clickBookmark(bm) { @@ -85,9 +92,15 @@ export default { }, submitUpdateBookmark(updatedBookmark) { var bookmark = { ...updatedBookmark } - bookmark.audiobookId = this.audiobookId - - this.$root.socket.emit('update_bookmark', bookmark) + this.$axios + .$patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark) + .then(() => { + this.$toast.success('Bookmark updated') + }) + .catch((error) => { + this.$toast.error(`Failed to update bookmark`) + console.error(error) + }) this.show = false }, submitCreateBookmark() { @@ -95,11 +108,18 @@ export default { this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm') } var bookmark = { - audiobookId: this.audiobookId, title: this.newBookmarkTitle, - time: this.currentTime + time: Math.floor(this.currentTime) } - this.$root.socket.emit('create_bookmark', bookmark) + this.$axios + .$post(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark) + .then(() => { + this.$toast.success('Bookmark added') + }) + .catch((error) => { + this.$toast.error(`Failed to create bookmark`) + console.error(error) + }) this.newBookmarkTitle = '' this.showBookmarkTitleInput = false diff --git a/client/store/user.js b/client/store/user.js index e8b9d700..da3826ae 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -26,6 +26,10 @@ export const getters = { if (!state.user.libraryItemProgress) return null return state.user.libraryItemProgress.find(li => li.id == libraryItemId) }, + getUserBookmarksForItem: (state) => (libraryItemId) => { + if (!state.user.bookmarks) return [] + return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId) + }, getUserSetting: (state) => (key) => { return state.settings ? state.settings[key] : null }, @@ -97,7 +101,6 @@ export const actions = { export const mutations = { setUser(state, user) { state.user = user - if (user) { if (user.token) localStorage.setItem('token', user.token) } else { diff --git a/server/Server.js b/server/Server.js index cf051b82..a51b512c 100644 --- a/server/Server.js +++ b/server/Server.js @@ -247,11 +247,6 @@ class Server { socket.on('create_backup', () => this.backupManager.requestCreateBackup(socket)) socket.on('apply_backup', (id) => this.backupManager.requestApplyBackup(socket, id)) - // Bookmarks - socket.on('create_bookmark', (payload) => this.createBookmark(socket, payload)) - socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload)) - socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload)) - socket.on('disconnect', () => { Logger.removeSocketListener(socket.id) @@ -459,91 +454,6 @@ class Server { res.sendStatus(200) } - // async audiobookProgressUpdate(socket, progressPayload) { - // var client = socket.sheepClient - // if (!client || !client.user) { - // Logger.error('[Server] audiobookProgressUpdate invalid socket client') - // return - // } - // var audiobookProgress = client.user.createUpdateLibraryItemProgress(progressPayload.audiobookId, progressPayload) - // if (audiobookProgress) { - // await this.db.updateEntity('user', client.user) - // this.clientEmitter(client.user.id, 'current_user_audiobook_update', { - // id: progressPayload.audiobookId, - // data: audiobookProgress || null - // }) - // } - // } - - async createBookmark(socket, payload) { - // var client = socket.sheepClient - // if (!client || !client.user) { - // Logger.error('[Server] createBookmark invalid socket client') - // return - // } - // var userAudiobook = client.user.createBookmark(payload) - // if (!userAudiobook || userAudiobook.error) { - // var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error' - // socket.emit('show_error_toast', `Failed to create Bookmark: ${failMessage}`) - // return - // } - - // await this.db.updateEntity('user', client.user) - - // socket.emit('show_success_toast', `${secondsToTimestamp(payload.time)} Bookmarked`) - - // this.clientEmitter(client.user.id, 'current_user_audiobook_update', { - // id: userAudiobook.audiobookId, - // data: userAudiobook || null - // }) - } - - async updateBookmark(socket, payload) { - // var client = socket.sheepClient - // if (!client || !client.user) { - // Logger.error('[Server] updateBookmark invalid socket client') - // return - // } - // var userAudiobook = client.user.updateBookmark(payload) - // if (!userAudiobook || userAudiobook.error) { - // var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error' - // socket.emit('show_error_toast', `Failed to update Bookmark: ${failMessage}`) - // return - // } - - // await this.db.updateEntity('user', client.user) - - // socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Updated`) - - // this.clientEmitter(client.user.id, 'current_user_audiobook_update', { - // id: userAudiobook.audiobookId, - // data: userAudiobook || null - // }) - } - - async deleteBookmark(socket, payload) { - // var client = socket.sheepClient - // if (!client || !client.user) { - // Logger.error('[Server] deleteBookmark invalid socket client') - // return - // } - // var userAudiobook = client.user.deleteBookmark(payload) - // if (!userAudiobook || userAudiobook.error) { - // var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error' - // socket.emit('show_error_toast', `Failed to delete Bookmark: ${failMessage}`) - // return - // } - - // await this.db.updateEntity('user', client.user) - - // socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Removed`) - - // this.clientEmitter(client.user.id, 'current_user_audiobook_update', { - // id: userAudiobook.audiobookId, - // data: userAudiobook || null - // }) - } - async authenticateSocket(socket, token) { var user = await this.auth.authenticateUser(token) if (!user) { diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index e979375f..6adaaabe 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -69,6 +69,50 @@ class MeController { res.sendStatus(200) } + // POST: api/me/item/:id/bookmark + async createBookmark(req, res) { + var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) + if (!libraryItem) return res.sendStatus(404) + const { time, title } = req.body + var bookmark = req.user.createBookmark(libraryItem.id, time, title) + await this.db.updateEntity('user', req.user) + this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + res.json(bookmark) + } + + // PATCH: api/me/item/:id/bookmark + async updateBookmark(req, res) { + var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) + if (!libraryItem) return res.sendStatus(404) + const { time, title } = req.body + if (!req.user.findBookmark(libraryItem.id, time)) { + Logger.error(`[MeController] updateBookmark not found`) + return res.sendStatus(404) + } + var bookmark = req.user.updateBookmark(libraryItem.id, time, title) + if (!bookmark) return res.sendStatus(500) + await this.db.updateEntity('user', req.user) + this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + res.json(bookmark) + } + + // DELETE: api/me/item/:id/bookmark/:time + async removeBookmark(req, res) { + var libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) + if (!libraryItem) return res.sendStatus(404) + var time = Number(req.params.time) + if (isNaN(time)) return res.sendStatus(500) + + if (!req.user.findBookmark(libraryItem.id, time)) { + Logger.error(`[MeController] removeBookmark not found`) + return res.sendStatus(404) + } + req.user.removeBookmark(libraryItem.id, time) + await this.db.updateEntity('user', req.user) + this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + res.sendStatus(200) + } + // PATCH: api/me/password updatePassword(req, res) { this.auth.userChangePassword(req, res) diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 8d2b05fb..2cd40273 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -98,6 +98,7 @@ class User { type: this.type, token: this.token, libraryItemProgress: this.libraryItemProgress ? this.libraryItemProgress.map(li => li.toJSON()) : [], + bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, lastSeen: this.lastSeen, @@ -136,7 +137,7 @@ class User { this.bookmarks = [] if (user.bookmarks) { - this.bookmarks = user.bookmarks.map(bm => new AudioBookmark(bm)) + this.bookmarks = user.bookmarks.filter(bm => typeof bm.libraryItemId == 'string').map(bm => new AudioBookmark(bm)) } this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive @@ -260,54 +261,35 @@ class User { return this.librariesAccessible.includes(libraryId) } - createBookmark({ libraryItemId, time, title }) { - // if (!this.audiobooks) this.audiobooks = {} - // if (!this.audiobooks[audiobookId]) { - // this.audiobooks[audiobookId] = new UserAudiobookData() - // this.audiobooks[audiobookId].audiobookId = audiobookId - // } - // if (this.audiobooks[audiobookId].checkBookmarkExists(time)) { - // return { - // error: 'Bookmark already exists' - // } - // } - - // var success = this.audiobooks[audiobookId].createBookmark(time, title) - // if (success) return this.audiobooks[audiobookId] - // return null + findBookmark(libraryItemId, time) { + return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time) } - updateBookmark({ audiobookId, time, title }) { - // if (!this.audiobooks || !this.audiobooks[audiobookId]) { - // return { - // error: 'Invalid Audiobook' - // } - // } - // if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) { - // return { - // error: 'Bookmark does not exist' - // } - // } - - // var success = this.audiobooks[audiobookId].updateBookmark(time, title) - // if (success) return this.audiobooks[audiobookId] - // return null + createBookmark(libraryItemId, time, title) { + var existingBookmark = this.findBookmark(libraryItemId, time) + if (existingBookmark) { + Logger.warn('[User] Create Bookmark already exists for this time') + existingBookmark.title = title + return existingBookmark + } + var newBookmark = new AudioBookmark() + newBookmark.setData(libraryItemId, time, title) + this.bookmarks.push(newBookmark) + return newBookmark } - deleteBookmark({ audiobookId, time }) { - // if (!this.audiobooks || !this.audiobooks[audiobookId]) { - // return { - // error: 'Invalid Audiobook' - // } - // } - // if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) { - // return { - // error: 'Bookmark does not exist' - // } - // } + updateBookmark(libraryItemId, time, title) { + var bookmark = this.findBookmark(libraryItemId, time) + if (!bookmark) { + Logger.error(`[User] updateBookmark not found`) + return null + } + bookmark.title = title + return bookmark + } - // this.audiobooks[audiobookId].deleteBookmark(time) - // return this.audiobooks[audiobookId] + removeBookmark(libraryItemId, time) { + this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time)) } syncLocalUserAudiobookData(localUserAudiobookData, audiobook) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 5c48bb79..db1d325d 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -134,6 +134,9 @@ class ApiRouter { this.router.patch('/me/progress/:id', MeController.createUpdateLibraryItemProgress.bind(this)) this.router.delete('/me/progress/:id', MeController.removeLibraryItemProgress.bind(this)) this.router.patch('/me/progress/batch/update', MeController.batchUpdateLibraryItemProgress.bind(this)) + this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this)) + this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this)) + this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this)) this.router.patch('/me/password', MeController.updatePassword.bind(this)) this.router.patch('/me/settings', MeController.updateSettings.bind(this)) diff --git a/server/utils/dbMigration.js b/server/utils/dbMigration.js index 549bb3b4..9134c638 100644 --- a/server/utils/dbMigration.js +++ b/server/utils/dbMigration.js @@ -262,9 +262,37 @@ async function migrateLibraryItems(db) { lip.libraryItemId = libraryItemWithAudiobook.id return lip }).filter(lip => !!lip) - await db.updateEntity('user', user) - Logger.debug(`>>> User ${user.username} with ${user.libraryItemProgress.length} progress entries were updated`) } + if (user.bookmarks.length) { + user.bookmarks = user.bookmarks.map((bookmark) => { + var audiobookId = bookmark.libraryItemId + var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(audiobookId)) + if (!libraryItemWithAudiobook) { + Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId) + return null + } + bookmark.libraryItemId = libraryItemWithAudiobook.id + return bookmark + }).filter(bm => !!bm) + } + if (user.libraryItemProgress.length || user.bookmarks.length) { + await db.updateEntity('user', user) + } + } + + // Update session LibraryItemId's + var sessions = await db.sessionsDb.select(() => true).then((results) => results.data) + if (sessions.length) { + sessions = sessions.map(se => { + var libraryItemWithAudiobook = libraryItems.find(li => li.media.getAudiobookById && !!li.media.getAudiobookById(se.mediaEntityId)) + if (!libraryItemWithAudiobook) { + Logger.error('[dbMigration] Failed to find library item with audiobook id', audiobookId) + return null + } + se.libraryItemId = libraryItemWithAudiobook.id + return se + }).filter(se => !!se) + await db.updateEntities('session', sessions) } Logger.info(`>>> ${libraryItems.length} Library Items made`) @@ -300,7 +328,7 @@ function cleanUserObject(db, userObj) { // Bookmarks now live on User.js object instead of inside UserAudiobookData if (userObj.audiobooks[audiobookId].bookmarks) { const cleanedBookmarks = userObj.audiobooks[audiobookId].bookmarks.map((bm) => { - bm.libraryItemId = audiobookId + bm.libraryItemId = audiobookId // Temp placeholder replace with libraryItemId when created return bm }) cleanedUserPayload.bookmarks = cleanedUserPayload.bookmarks.concat(cleanedBookmarks) @@ -308,7 +336,7 @@ function cleanUserObject(db, userObj) { var userAudiobookData = new UserAudiobookData(userObj.audiobooks[audiobookId]) // Legacy object var liProgress = new LibraryItemProgress() // New Progress Object - liProgress.id = userAudiobookData.audiobookId // This ID is INCORRECT, will be updated when library item is created + liProgress.id = userAudiobookData.audiobookId // This ID will be updated when library item is created liProgress.libraryItemId = userAudiobookData.audiobookId liProgress.isFinished = !!userAudiobookData.isRead Object.keys(liProgress.toJSON()).forEach((key) => { @@ -333,9 +361,10 @@ function cleanUserObject(db, userObj) { function cleanSessionObj(db, userListeningSession) { var newPlaybackSession = new PlaybackSession(userListeningSession) + newPlaybackSession.id = getId('play') newPlaybackSession.mediaType = 'book' newPlaybackSession.updatedAt = userListeningSession.lastUpdate - newPlaybackSession.libraryItemId = userListeningSession.audiobookId + newPlaybackSession.libraryItemId = userListeningSession.audiobookId // Temp newPlaybackSession.mediaEntityId = userListeningSession.audiobookId newPlaybackSession.playMethod = PlayMethod.TRANSCODE