From f9e6655359c30a7c76ab95c2c57c086a6cdfaec8 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 5 Feb 2023 16:52:17 -0600 Subject: [PATCH] Update:API endpoint for syncing multiple local sessions. New API endpoint to get current user. Deprecate /me/sync-local-progress endpoint --- server/controllers/MeController.js | 5 + server/controllers/SessionController.js | 5 + server/managers/PlaybackSessionManager.js | 121 ++++++++++++++-------- server/objects/PlaybackSession.js | 14 +++ server/routers/ApiRouter.js | 8 +- 5 files changed, 107 insertions(+), 46 deletions(-) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 7e4cfc09..1286cc12 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -6,6 +6,10 @@ const { isObject, toNumber } = require('../utils/index') class MeController { constructor() { } + getCurrentUser(req, res) { + res.json(req.user.toJSONForBrowser()) + } + // GET: api/me/listening-sessions async getListeningSessions(req, res) { var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id) @@ -184,6 +188,7 @@ class MeController { }) } + // TODO: Deprecated. Removed from Android. Only used in iOS app now. // POST: api/me/sync-local-progress async syncLocalMediaProgress(req, res) { if (!req.body.localMediaProgress) { diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 346ffa03..721fbd27 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -75,6 +75,11 @@ class SessionController { this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res) } + // POST: api/session/local-all + syncLocalSessions(req, res) { + this.playbackSessionManager.syncLocalSessionsRequest(req, res) + } + openSessionMiddleware(req, res, next) { var playbackSession = this.playbackSessionManager.getSession(req.params.id) if (!playbackSession) return res.sendStatus(404) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index b0d935f4..30b4ad24 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -21,7 +21,6 @@ class PlaybackSessionManager { this.StreamsPath = Path.join(global.MetadataPath, 'streams') this.sessions = [] - this.localSessionLock = {} } getSession(sessionId) { @@ -61,18 +60,84 @@ class PlaybackSessionManager { } } - async syncLocalSessionRequest(user, sessionJson, res) { - if (this.localSessionLock[sessionJson.id]) { - Logger.debug(`[PlaybackSessionManager] syncLocalSessionRequest: Local session is locked and already syncing`) - return res.status(500).send('Local session is locked and already syncing') + async syncLocalSessionsRequest(req, res) { + const user = req.user + const sessions = req.body.sessions || [] + + const syncResults = [] + for (const sessionJson of sessions) { + Logger.info(`[PlaybackSessionManager] Syncing local session "${sessionJson.displayTitle}" (${sessionJson.id})`) + const result = await this.syncLocalSession(user, sessionJson) + syncResults.push(result) } + res.json({ + results: syncResults + }) + } + + async syncLocalSession(user, sessionJson) { const libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId) - if (!libraryItem) { - Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`) - return res.status(500).send('Library item not found') + const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null + if (!libraryItem || (libraryItem.isPodcast && !episode)) { + Logger.error(`[PlaybackSessionManager] syncLocalSession: Media item not found for session "${sessionJson.displayTitle}" (${sessionJson.id})`) + return { + id: sessionJson.id, + success: false, + error: 'Media item not found' + } } + let session = await this.db.getPlaybackSession(sessionJson.id) + if (!session) { + // New session from local + session = new PlaybackSession(sessionJson) + Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) + await this.db.insertEntity('session', session) + } else { + session.currentTime = sessionJson.currentTime + session.timeListening = sessionJson.timeListening + session.updatedAt = sessionJson.updatedAt + session.date = date.format(new Date(), 'YYYY-MM-DD') + session.dayOfWeek = date.format(new Date(), 'dddd') + + Logger.debug(`[PlaybackSessionManager] Updated session for "${session.displayTitle}" (${session.id})`) + await this.db.updateEntity('session', session) + } + + const result = { + id: session.id, + success: true, + progressSynced: false + } + + const userProgressForItem = user.getMediaProgress(session.libraryItemId, session.episodeId) + if (userProgressForItem) { + if (userProgressForItem.lastUpdate > session.updatedAt) { + Logger.debug(`[PlaybackSessionManager] Not updating progress for "${session.displayTitle}" because it has been updated more recently`) + } else { + Logger.debug(`[PlaybackSessionManager] Updating progress for "${session.displayTitle}" with current time ${session.currentTime} (previously ${userProgressForItem.currentTime})`) + result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId) + } + } else { + Logger.debug(`[PlaybackSessionManager] Creating new media progress for media item "${session.displayTitle}"`) + result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId) + } + + // Update user and emit socket event + if (result.progressSynced) { + await this.db.updateEntity('user', user) + const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) + SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { + id: itemProgress.id, + data: itemProgress.toJSON() + }) + } + + return result + } + + async syncLocalSessionRequest(user, sessionJson, res) { // If server session is open for this same media item then close it const userSessionForThisItem = this.sessions.find(playbackSession => { if (playbackSession.userId !== user.id) return false @@ -84,43 +149,13 @@ class PlaybackSessionManager { await this.closeSession(user, userSessionForThisItem, null) } - this.localSessionLock[sessionJson.id] = true // Lock local session - - let session = await this.db.getPlaybackSession(sessionJson.id) - if (!session) { - // New session from local - session = new PlaybackSession(sessionJson) - await this.db.insertEntity('session', session) + // Sync + const result = await this.syncLocalSession(user, sessionJson) + if (result.error) { + res.status(500).send(result.error) } else { - session.currentTime = sessionJson.currentTime - session.timeListening = sessionJson.timeListening - session.updatedAt = sessionJson.updatedAt - session.date = date.format(new Date(), 'YYYY-MM-DD') - session.dayOfWeek = date.format(new Date(), 'dddd') - await this.db.updateEntity('session', session) + res.sendStatus(200) } - - session.currentTime = sessionJson.currentTime - - const itemProgressUpdate = { - duration: session.duration, - currentTime: session.currentTime, - progress: session.progress, - lastUpdate: session.updatedAt // Keep media progress update times the same as local - } - const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) - if (wasUpdated) { - await this.db.updateEntity('user', user) - const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) - SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { - id: itemProgress.id, - data: itemProgress.toJSON() - }) - } - - delete this.localSessionLock[sessionJson.id] // Unlock local session - - res.sendStatus(200) } async closeSessionRequest(user, session, syncData, res) { diff --git a/server/objects/PlaybackSession.js b/server/objects/PlaybackSession.js index be50f57e..db19a8d2 100644 --- a/server/objects/PlaybackSession.js +++ b/server/objects/PlaybackSession.js @@ -141,6 +141,11 @@ class PlaybackSession { this.updatedAt = session.updatedAt || null } + get mediaItemId() { + if (this.episodeId) return `${this.libraryItemId}-${this.episodeId}` + return this.libraryItemId + } + get progress() { // Value between 0 and 1 if (!this.duration) return 0 return Math.max(0, Math.min(this.currentTime / this.duration, 1)) @@ -151,6 +156,15 @@ class PlaybackSession { return this.deviceInfo.deviceDescription } + get mediaProgressObject() { + return { + duration: this.duration, + currentTime: this.currentTime, + progress: this.progress, + lastUpdate: this.updatedAt + } + } + setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) { this.id = getId('play') this.userId = user.id diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 42a91214..41c26769 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -162,6 +162,7 @@ class ApiRouter { // // Current User Routes (Me) // + this.router.get('/me', MeController.getCurrentUser.bind(this)) this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this)) this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this)) this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this)) @@ -174,8 +175,8 @@ class ApiRouter { 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)) // TODO: Remove after mobile release v0.9.61-beta - this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this)) + this.router.patch('/me/settings', MeController.updateSettings.bind(this)) // TODO: Deprecated. Remove after mobile release v0.9.61-beta + this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this)) // TODO: Deprecated. Removed from Android. Only used in iOS app now. this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this)) this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this)) @@ -215,11 +216,12 @@ class ApiRouter { // this.router.get('/sessions', SessionController.getAllWithUserData.bind(this)) this.router.delete('/sessions/:id', SessionController.middleware.bind(this), SessionController.delete.bind(this)) + this.router.post('/session/local', SessionController.syncLocal.bind(this)) + this.router.post('/session/local-all', SessionController.syncLocalSessions.bind(this)) // TODO: Update these endpoints because they are only for open playback sessions this.router.get('/session/:id', SessionController.openSessionMiddleware.bind(this), SessionController.getOpenSession.bind(this)) this.router.post('/session/:id/sync', SessionController.openSessionMiddleware.bind(this), SessionController.sync.bind(this)) this.router.post('/session/:id/close', SessionController.openSessionMiddleware.bind(this), SessionController.close.bind(this)) - this.router.post('/session/local', SessionController.syncLocal.bind(this)) // // Podcast Routes