Add sync local media progress routes for offline mobile playback session support

This commit is contained in:
advplyr 2022-04-09 17:56:51 -05:00
parent fc228013d3
commit 2a386ca2a9
7 changed files with 113 additions and 63 deletions

View File

@ -13,6 +13,7 @@ const Library = require('./objects/Library')
const Author = require('./objects/entities/Author')
const Series = require('./objects/entities/Series')
const ServerSettings = require('./objects/ServerSettings')
const PlaybackSession = require('./objects/PlaybackSession')
class Db {
constructor() {
@ -188,6 +189,17 @@ class Db {
getLibraryItem(id) {
return this.libraryItems.find(li => li.id === id)
}
getPlaybackSession(id) {
return this.sessionsDb.select((pb) => pb.id == id).then((results) => {
if (results.data.length) {
return new PlaybackSession(results.data[0])
}
return null
}).catch((error) => {
Logger.error('Failed to get session', error)
return null
})
}
async updateLibraryItem(libraryItem) {
return this.updateLibraryItems([libraryItem])

View File

@ -151,5 +151,63 @@ class MeController {
settings: req.user.settings
})
}
// POST: api/me/sync-local-progress
async syncLocalMediaProgress(req, res) {
if (!req.body.localMediaProgress) {
Logger.error(`[MeController] syncLocalMediaProgress invalid post body`)
return res.sendStatus(500)
}
const updatedLocalMediaProgress = []
var numServerProgressUpdates = 0
var localMediaProgress = req.body.localMediaProgress || []
localMediaProgress.forEach(localProgress => {
if (!localProgress.libraryItemId) {
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
return
}
var libraryItem = this.db.getLibraryItem(localProgress.libraryItemId)
if (!libraryItem) {
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item`, localProgress)
return
}
var mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
if (!mediaProgress) {
// New media progress from mobile
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
numServerProgressUpdates++
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
numServerProgressUpdates++
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
var updateTimeDifference = mediaProgress.lastUpdate - localProgress.lastUpdate
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
for (const key in localProgress) {
if (mediaProgress[key] != undefined && localProgress[key] !== mediaProgress[key]) {
// Logger.debug(`[MeController] syncLocalMediaProgress key ${key} changed from ${localProgress[key]} to ${mediaProgress[key]} - ${mediaProgress.id}`)
localProgress[key] = mediaProgress[key]
}
}
updatedLocalMediaProgress.push(localProgress)
} else {
Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`)
}
})
Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`)
if (numServerProgressUpdates > 0) {
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.json({
numServerProgressUpdates,
localProgressUpdates: updatedLocalMediaProgress
})
}
}
module.exports = new MeController()

View File

@ -17,6 +17,11 @@ class SessionController {
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
}
// POST: api/session/local
syncLocal(req, res) {
this.playbackSessionManager.syncLocalSessionRequest(req.user, req.body, res)
}
middleware(req, res, next) {
var playbackSession = this.playbackSessionManager.getSession(req.params.id)
if (!playbackSession) return res.sendStatus(404)

View File

@ -37,6 +37,40 @@ class PlaybackSessionManager {
}
}
async syncLocalSessionRequest(user, sessionJson, res) {
var libraryItem = this.db.getLibraryItem(sessionJson.libraryItemId)
var session = await this.db.getPlaybackSession(sessionJson.id)
if (!session) {
// New session from local
session = new PlaybackSession(sessionJson)
await this.db.insertEntity('session', session)
} else {
session.timeListening = sessionJson.timeListening
session.updatedAt = sessionJson.updatedAt
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
}
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
if (wasUpdated) {
await this.db.updateEntity('user', user)
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
this.clientEmitter(user.id, 'user_item_progress_updated', {
id: itemProgress.id,
data: itemProgress.toJSON()
})
}
res.sendStatus(200)
}
async closeSessionRequest(user, session, syncData, res) {
await this.closeSession(user, session, syncData)
res.sendStatus(200)

View File

@ -264,6 +264,8 @@ class User {
return true
}
var wasUpdated = itemProgress.update(updatePayload)
if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync
return wasUpdated
}
@ -340,32 +342,5 @@ class User {
removeBookmark(libraryItemId, time) {
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time))
}
// TODO: re-do mobile sync
syncLocalUserAudiobookData(localUserAudiobookData, audiobook) {
// if (!localUserAudiobookData || !localUserAudiobookData.audiobookId) {
// Logger.error(`[User] Invalid local user audiobook data`, localUserAudiobookData)
// return false
// }
// if (!this.audiobooks) this.audiobooks = {}
// if (!this.audiobooks[localUserAudiobookData.audiobookId]) {
// this.audiobooks[localUserAudiobookData.audiobookId] = new UserAudiobookData(localUserAudiobookData)
// return true
// }
// var userAbD = this.audiobooks[localUserAudiobookData.audiobookId]
// if (userAbD.lastUpdate >= localUserAudiobookData.lastUpdate) {
// // Server audiobook data is more recent
// return false
// }
// // Local Data More recent
// var wasUpdated = this.audiobooks[localUserAudiobookData.audiobookId].update(localUserAudiobookData)
// if (wasUpdated) {
// Logger.debug(`[User] syncLocalUserAudiobookData local data was more recent for "${audiobook.title}"`)
// }
// return wasUpdated
}
}
module.exports = User

View File

@ -135,6 +135,7 @@ class ApiRouter {
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))
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
//
// Backup Routes
@ -169,6 +170,7 @@ class ApiRouter {
//
this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
this.router.post('/session/local', SessionController.syncLocal.bind(this))
//
// Podcast Routes
@ -192,9 +194,6 @@ class ApiRouter {
this.router.get('/search/podcast', MiscController.findPodcasts.bind(this))
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
this.router.get('/tags', MiscController.getAllTags.bind(this))
// OLD
// this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
}
async getDirectories(dir, relpath, excludedDirs, level = 0) {
@ -226,38 +225,6 @@ class ApiRouter {
}
}
async syncUserAudiobookData(req, res) {
// if (!req.body.data) {
// return res.status(403).send('Invalid local user audiobook data')
// }
// var hasUpdates = false
// // Local user audiobook data use the latest update
// req.body.data.forEach((uab) => {
// if (!uab || !uab.audiobookId) {
// Logger.error('[ApiController] Invalid user audiobook data', uab)
// return
// }
// var audiobook = this.db.audiobooks.find(ab => ab.id === uab.audiobookId)
// if (!audiobook) {
// Logger.info('[ApiController] syncUserAudiobookData local audiobook data audiobook no longer exists', uab.audiobookId)
// return
// }
// if (req.user.syncLocalUserAudiobookData(uab, audiobook)) {
// this.clientEmitter(req.user.id, 'current_user_audiobook_update', { id: uab.audiobookId, data: uab })
// hasUpdates = true
// }
// })
// if (hasUpdates) {
// await this.db.updateEntity('user', req.user)
// }
// var allUserAudiobookData = Object.values(req.user.audiobooksToJSON())
// res.json(allUserAudiobookData)
}
//
// Helper Methods
//

View File

@ -18,7 +18,6 @@ class StaticRouter {
var remainingPath = req.params['0']
var fullPath = Path.join(item.path, remainingPath)
console.log('fullpath', fullPath)
res.sendFile(fullPath)
})
}