mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-11 16:28:17 +02:00
Add sync local media progress routes for offline mobile playback session support
This commit is contained in:
parent
fc228013d3
commit
2a386ca2a9
12
server/Db.js
12
server/Db.js
@ -13,6 +13,7 @@ const Library = require('./objects/Library')
|
|||||||
const Author = require('./objects/entities/Author')
|
const Author = require('./objects/entities/Author')
|
||||||
const Series = require('./objects/entities/Series')
|
const Series = require('./objects/entities/Series')
|
||||||
const ServerSettings = require('./objects/ServerSettings')
|
const ServerSettings = require('./objects/ServerSettings')
|
||||||
|
const PlaybackSession = require('./objects/PlaybackSession')
|
||||||
|
|
||||||
class Db {
|
class Db {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -188,6 +189,17 @@ class Db {
|
|||||||
getLibraryItem(id) {
|
getLibraryItem(id) {
|
||||||
return this.libraryItems.find(li => li.id === 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) {
|
async updateLibraryItem(libraryItem) {
|
||||||
return this.updateLibraryItems([libraryItem])
|
return this.updateLibraryItems([libraryItem])
|
||||||
|
@ -151,5 +151,63 @@ class MeController {
|
|||||||
settings: req.user.settings
|
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()
|
module.exports = new MeController()
|
@ -17,6 +17,11 @@ class SessionController {
|
|||||||
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
|
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) {
|
middleware(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)
|
||||||
|
@ -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) {
|
async closeSessionRequest(user, session, syncData, res) {
|
||||||
await this.closeSession(user, session, syncData)
|
await this.closeSession(user, session, syncData)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
|
@ -264,6 +264,8 @@ class User {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
var wasUpdated = itemProgress.update(updatePayload)
|
var wasUpdated = itemProgress.update(updatePayload)
|
||||||
|
|
||||||
|
if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync
|
||||||
return wasUpdated
|
return wasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,32 +342,5 @@ class User {
|
|||||||
removeBookmark(libraryItemId, time) {
|
removeBookmark(libraryItemId, time) {
|
||||||
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== 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
|
module.exports = User
|
@ -135,6 +135,7 @@ class ApiRouter {
|
|||||||
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))
|
this.router.patch('/me/settings', MeController.updateSettings.bind(this))
|
||||||
|
this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Backup Routes
|
// 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/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/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
|
||||||
|
this.router.post('/session/local', SessionController.syncLocal.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Podcast Routes
|
// Podcast Routes
|
||||||
@ -192,9 +194,6 @@ class ApiRouter {
|
|||||||
this.router.get('/search/podcast', MiscController.findPodcasts.bind(this))
|
this.router.get('/search/podcast', MiscController.findPodcasts.bind(this))
|
||||||
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
|
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
|
||||||
this.router.get('/tags', MiscController.getAllTags.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) {
|
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
|
// Helper Methods
|
||||||
//
|
//
|
||||||
|
@ -18,7 +18,6 @@ class StaticRouter {
|
|||||||
|
|
||||||
var remainingPath = req.params['0']
|
var remainingPath = req.params['0']
|
||||||
var fullPath = Path.join(item.path, remainingPath)
|
var fullPath = Path.join(item.path, remainingPath)
|
||||||
console.log('fullpath', fullPath)
|
|
||||||
res.sendFile(fullPath)
|
res.sendFile(fullPath)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user