Update:API endpoint for syncing multiple local sessions. New API endpoint to get current user. Deprecate /me/sync-local-progress endpoint

This commit is contained in:
advplyr 2023-02-05 16:52:17 -06:00
parent debf0f495d
commit f9e6655359
5 changed files with 107 additions and 46 deletions

View File

@ -6,6 +6,10 @@ const { isObject, toNumber } = require('../utils/index')
class MeController { class MeController {
constructor() { } constructor() { }
getCurrentUser(req, res) {
res.json(req.user.toJSONForBrowser())
}
// GET: api/me/listening-sessions // GET: api/me/listening-sessions
async getListeningSessions(req, res) { async getListeningSessions(req, res) {
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id) 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 // POST: api/me/sync-local-progress
async syncLocalMediaProgress(req, res) { async syncLocalMediaProgress(req, res) {
if (!req.body.localMediaProgress) { if (!req.body.localMediaProgress) {

View File

@ -75,6 +75,11 @@ class SessionController {
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res) 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) { openSessionMiddleware(req, res, next) {
var playbackSession = this.playbackSessionManager.getSession(req.params.id) var playbackSession = this.playbackSessionManager.getSession(req.params.id)
if (!playbackSession) return res.sendStatus(404) if (!playbackSession) return res.sendStatus(404)

View File

@ -21,7 +21,6 @@ class PlaybackSessionManager {
this.StreamsPath = Path.join(global.MetadataPath, 'streams') this.StreamsPath = Path.join(global.MetadataPath, 'streams')
this.sessions = [] this.sessions = []
this.localSessionLock = {}
} }
getSession(sessionId) { getSession(sessionId) {
@ -61,18 +60,84 @@ class PlaybackSessionManager {
} }
} }
async syncLocalSessionRequest(user, sessionJson, res) { async syncLocalSessionsRequest(req, res) {
if (this.localSessionLock[sessionJson.id]) { const user = req.user
Logger.debug(`[PlaybackSessionManager] syncLocalSessionRequest: Local session is locked and already syncing`) const sessions = req.body.sessions || []
return res.status(500).send('Local session is locked and already syncing')
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) const libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
if (!libraryItem) { const episode = (sessionJson.episodeId && libraryItem && libraryItem.isPodcast) ? libraryItem.media.getEpisode(sessionJson.episodeId) : null
Logger.error(`[PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session "${sessionJson.libraryItemId}"`) if (!libraryItem || (libraryItem.isPodcast && !episode)) {
return res.status(500).send('Library item not found') 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 // If server session is open for this same media item then close it
const userSessionForThisItem = this.sessions.find(playbackSession => { const userSessionForThisItem = this.sessions.find(playbackSession => {
if (playbackSession.userId !== user.id) return false if (playbackSession.userId !== user.id) return false
@ -84,44 +149,14 @@ class PlaybackSessionManager {
await this.closeSession(user, userSessionForThisItem, null) await this.closeSession(user, userSessionForThisItem, null)
} }
this.localSessionLock[sessionJson.id] = true // Lock local session // Sync
const result = await this.syncLocalSession(user, sessionJson)
let session = await this.db.getPlaybackSession(sessionJson.id) if (result.error) {
if (!session) { res.status(500).send(result.error)
// New session from local
session = new PlaybackSession(sessionJson)
await this.db.insertEntity('session', session)
} else { } 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)
}
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) res.sendStatus(200)
} }
}
async closeSessionRequest(user, session, syncData, res) { async closeSessionRequest(user, session, syncData, res) {
await this.closeSession(user, session, syncData) await this.closeSession(user, session, syncData)

View File

@ -141,6 +141,11 @@ class PlaybackSession {
this.updatedAt = session.updatedAt || null 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 get progress() { // Value between 0 and 1
if (!this.duration) return 0 if (!this.duration) return 0
return Math.max(0, Math.min(this.currentTime / this.duration, 1)) return Math.max(0, Math.min(this.currentTime / this.duration, 1))
@ -151,6 +156,15 @@ class PlaybackSession {
return this.deviceInfo.deviceDescription 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) { setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) {
this.id = getId('play') this.id = getId('play')
this.userId = user.id this.userId = user.id

View File

@ -162,6 +162,7 @@ class ApiRouter {
// //
// Current User Routes (Me) // 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-sessions', MeController.getListeningSessions.bind(this))
this.router.get('/me/listening-stats', MeController.getListeningStats.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)) 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.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.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/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.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)) 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/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/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))
this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.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.get('/sessions', SessionController.getAllWithUserData.bind(this))
this.router.delete('/sessions/:id', SessionController.middleware.bind(this), SessionController.delete.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 // 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.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/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/:id/close', SessionController.openSessionMiddleware.bind(this), SessionController.close.bind(this))
this.router.post('/session/local', SessionController.syncLocal.bind(this))
// //
// Podcast Routes // Podcast Routes