mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-26 08:12:25 -04:00 
			
		
		
		
	Update listening sessions per device and show open sessions
This commit is contained in:
		
							parent
							
								
									8fca84e4bd
								
							
						
					
					
						commit
						25ca950dd0
					
				| @ -460,6 +460,13 @@ export default { | |||||||
|     showFailedProgressSyncs() { |     showFailedProgressSyncs() { | ||||||
|       if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast) |       if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast) | ||||||
|       this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' }) |       this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' }) | ||||||
|  |     }, | ||||||
|  |     sessionClosedEvent(sessionId) { | ||||||
|  |       if (this.playerHandler.currentSessionId === sessionId) { | ||||||
|  |         console.log('sessionClosedEvent closing current session', sessionId) | ||||||
|  |         this.playerHandler.resetPlayer() // Closes player without reporting to server | ||||||
|  |         this.$store.commit('setMediaPlaying', null) | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|  | |||||||
| @ -98,7 +98,8 @@ | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div class="flex items-center"> |       <div class="flex items-center"> | ||||||
|         <ui-btn small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> |         <ui-btn v-if="!isOpenSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> | ||||||
|  |         <ui-btn v-else small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </modals-modal> |   </modals-modal> | ||||||
| @ -157,6 +158,9 @@ export default { | |||||||
|     }, |     }, | ||||||
|     timeFormat() { |     timeFormat() { | ||||||
|       return this.$store.state.serverSettings.timeFormat |       return this.$store.state.serverSettings.timeFormat | ||||||
|  |     }, | ||||||
|  |     isOpenSession() { | ||||||
|  |       return !!this._session.open | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
| @ -188,6 +192,24 @@ export default { | |||||||
|           var errMsg = error.response ? error.response.data || '' : '' |           var errMsg = error.response ? error.response.data || '' : '' | ||||||
|           this.$toast.error(errMsg || this.$strings.ToastSessionDeleteFailed) |           this.$toast.error(errMsg || this.$strings.ToastSessionDeleteFailed) | ||||||
|         }) |         }) | ||||||
|  |     }, | ||||||
|  |     closeSessionClick() { | ||||||
|  |       this.processing = true | ||||||
|  |       this.$axios | ||||||
|  |         .$post(`/api/session/${this._session.id}/close`) | ||||||
|  |         .then(() => { | ||||||
|  |           this.$toast.success('Session closed') | ||||||
|  |           this.show = false | ||||||
|  |           this.$emit('closedSession') | ||||||
|  |         }) | ||||||
|  |         .catch((error) => { | ||||||
|  |           console.error('Failed to close session', error) | ||||||
|  |           const errMsg = error.response?.data || '' | ||||||
|  |           this.$toast.error(errMsg || 'Failed to close open session') | ||||||
|  |         }) | ||||||
|  |         .finally(() => { | ||||||
|  |           this.processing = false | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() {} |   mounted() {} | ||||||
|  | |||||||
| @ -299,8 +299,17 @@ export default { | |||||||
|     userStreamUpdate(user) { |     userStreamUpdate(user) { | ||||||
|       this.$store.commit('users/updateUserOnline', user) |       this.$store.commit('users/updateUserOnline', user) | ||||||
|     }, |     }, | ||||||
|  |     userSessionClosed(sessionId) { | ||||||
|  |       if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId) | ||||||
|  |     }, | ||||||
|     userMediaProgressUpdate(payload) { |     userMediaProgressUpdate(payload) { | ||||||
|       this.$store.commit('user/updateMediaProgress', payload) |       this.$store.commit('user/updateMediaProgress', payload) | ||||||
|  | 
 | ||||||
|  |       if (payload.data) { | ||||||
|  |         if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId)) { | ||||||
|  |           // TODO: Update currently open session if being played from another device | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     collectionAdded(collection) { |     collectionAdded(collection) { | ||||||
|       if (this.currentLibraryId !== collection.libraryId) return |       if (this.currentLibraryId !== collection.libraryId) return | ||||||
| @ -405,6 +414,7 @@ export default { | |||||||
|       this.socket.on('user_online', this.userOnline) |       this.socket.on('user_online', this.userOnline) | ||||||
|       this.socket.on('user_offline', this.userOffline) |       this.socket.on('user_offline', this.userOffline) | ||||||
|       this.socket.on('user_stream_update', this.userStreamUpdate) |       this.socket.on('user_stream_update', this.userStreamUpdate) | ||||||
|  |       this.socket.on('user_session_closed', this.userSessionClosed) | ||||||
|       this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate) |       this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate) | ||||||
| 
 | 
 | ||||||
|       // Collection Listeners |       // Collection Listeners | ||||||
|  | |||||||
| @ -52,9 +52,53 @@ | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p> |       <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p> | ||||||
|  | 
 | ||||||
|  |       <!-- open listening sessions table --> | ||||||
|  |       <p v-if="openListeningSessions.length" class="text-lg mb-4 mt-8">Open Listening Sessions</p> | ||||||
|  |       <div v-if="openListeningSessions.length" class="block max-w-full"> | ||||||
|  |         <table class="userSessionsTable"> | ||||||
|  |           <tr class="bg-primary bg-opacity-40"> | ||||||
|  |             <th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th> | ||||||
|  |             <th class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th> | ||||||
|  |             <th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th> | ||||||
|  |             <th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th> | ||||||
|  |             <th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th> | ||||||
|  |             <th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th> | ||||||
|  |             <th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th> | ||||||
|  |           </tr> | ||||||
|  | 
 | ||||||
|  |           <tr v-for="session in openListeningSessions" :key="`open-${session.id}`" class="cursor-pointer" @click="showSession(session)"> | ||||||
|  |             <td class="py-1 max-w-48"> | ||||||
|  |               <p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p> | ||||||
|  |               <p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p> | ||||||
|  |             </td> | ||||||
|  |             <td class="hidden md:table-cell"> | ||||||
|  |               <p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p> | ||||||
|  |               <p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p> | ||||||
|  |             </td> | ||||||
|  |             <td class="hidden md:table-cell"> | ||||||
|  |               <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> | ||||||
|  |             </td> | ||||||
|  |             <td class="hidden sm:table-cell"> | ||||||
|  |               <p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" /> | ||||||
|  |             </td> | ||||||
|  |             <td class="text-center"> | ||||||
|  |               <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> | ||||||
|  |             </td> | ||||||
|  |             <td class="text-center hover:underline" @click.stop="clickCurrentTime(session)"> | ||||||
|  |               <p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p> | ||||||
|  |             </td> | ||||||
|  |             <td class="text-center hidden sm:table-cell"> | ||||||
|  |               <ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)"> | ||||||
|  |                 <p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p> | ||||||
|  |               </ui-tooltip> | ||||||
|  |             </td> | ||||||
|  |           </tr> | ||||||
|  |         </table> | ||||||
|  |       </div> | ||||||
|     </app-settings-content> |     </app-settings-content> | ||||||
| 
 | 
 | ||||||
|     <modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" /> |     <modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" @closedSession="closedSession" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| @ -81,6 +125,7 @@ export default { | |||||||
|       showSessionModal: false, |       showSessionModal: false, | ||||||
|       selectedSession: null, |       selectedSession: null, | ||||||
|       listeningSessions: [], |       listeningSessions: [], | ||||||
|  |       openListeningSessions: [], | ||||||
|       numPages: 0, |       numPages: 0, | ||||||
|       total: 0, |       total: 0, | ||||||
|       currentPage: 0, |       currentPage: 0, | ||||||
| @ -114,6 +159,9 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     closedSession() { | ||||||
|  |       this.loadOpenSessions() | ||||||
|  |     }, | ||||||
|     removedSession() { |     removedSession() { | ||||||
|       // If on last page and this was the last session then load prev page |       // If on last page and this was the last session then load prev page | ||||||
|       if (this.currentPage == this.numPages - 1) { |       if (this.currentPage == this.numPages - 1) { | ||||||
| @ -222,7 +270,7 @@ export default { | |||||||
|     async loadSessions(page) { |     async loadSessions(page) { | ||||||
|       var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : '' |       var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : '' | ||||||
|       const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => { |       const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => { | ||||||
|         console.error('Failed to load listening sesions', err) |         console.error('Failed to load listening sessions', err) | ||||||
|         return null |         return null | ||||||
|       }) |       }) | ||||||
|       if (!data) { |       if (!data) { | ||||||
| @ -236,8 +284,24 @@ export default { | |||||||
|       this.listeningSessions = data.sessions |       this.listeningSessions = data.sessions | ||||||
|       this.userFilter = data.userFilter |       this.userFilter = data.userFilter | ||||||
|     }, |     }, | ||||||
|  |     async loadOpenSessions() { | ||||||
|  |       const data = await this.$axios.$get('/api/sessions/open').catch((err) => { | ||||||
|  |         console.error('Failed to load open sessions', err) | ||||||
|  |         return null | ||||||
|  |       }) | ||||||
|  |       if (!data) { | ||||||
|  |         this.$toast.error('Failed to load open sessions') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.openListeningSessions = (data.sessions || []).map((s) => { | ||||||
|  |         s.open = true | ||||||
|  |         return s | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|     init() { |     init() { | ||||||
|       this.loadSessions(0) |       this.loadSessions(0) | ||||||
|  |       this.loadOpenSessions() | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|  | |||||||
| @ -173,16 +173,28 @@ export default class PlayerHandler { | |||||||
|     this.ctx.setBufferTime(buffertime) |     this.ctx.setBufferTime(buffertime) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   getDeviceId() { | ||||||
|  |     let deviceId = localStorage.getItem('absDeviceId') | ||||||
|  |     if (!deviceId) { | ||||||
|  |       deviceId = this.ctx.$randomId() | ||||||
|  |       localStorage.setItem('absDeviceId', deviceId) | ||||||
|  |     } | ||||||
|  |     return deviceId | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async prepare(forceTranscode = false) { |   async prepare(forceTranscode = false) { | ||||||
|     var payload = { |     const payload = { | ||||||
|  |       deviceInfo: { | ||||||
|  |         deviceId: this.getDeviceId() | ||||||
|  |       }, | ||||||
|       supportedMimeTypes: this.player.playableMimeTypes, |       supportedMimeTypes: this.player.playableMimeTypes, | ||||||
|       mediaPlayer: this.isCasting ? 'chromecast' : 'html5', |       mediaPlayer: this.isCasting ? 'chromecast' : 'html5', | ||||||
|       forceTranscode, |       forceTranscode, | ||||||
|       forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast
 |       forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play` |     const path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play` | ||||||
|     var session = await this.ctx.$axios.$post(path, payload).catch((error) => { |     const session = await this.ctx.$axios.$post(path, payload).catch((error) => { | ||||||
|       console.error('Failed to start stream', error) |       console.error('Failed to start stream', error) | ||||||
|     }) |     }) | ||||||
|     this.prepareSession(session) |     this.prepareSession(session) | ||||||
| @ -238,6 +250,10 @@ export default class PlayerHandler { | |||||||
|   closePlayer() { |   closePlayer() { | ||||||
|     console.log('[PlayerHandler] Close Player') |     console.log('[PlayerHandler] Close Player') | ||||||
|     this.sendCloseSession() |     this.sendCloseSession() | ||||||
|  |     this.resetPlayer() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   resetPlayer() { | ||||||
|     if (this.player) { |     if (this.player) { | ||||||
|       this.player.destroy() |       this.player.destroy() | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,5 +1,8 @@ | |||||||
| import Vue from 'vue' | import Vue from 'vue' | ||||||
| import cronParser from 'cron-parser' | import cronParser from 'cron-parser' | ||||||
|  | import { nanoid } from 'nanoid' | ||||||
|  | 
 | ||||||
|  | Vue.prototype.$randomId = () => nanoid() | ||||||
| 
 | 
 | ||||||
| Vue.prototype.$bytesPretty = (bytes, decimals = 2) => { | Vue.prototype.$bytesPretty = (bytes, decimals = 2) => { | ||||||
|   if (isNaN(bytes) || bytes == 0) { |   if (isNaN(bytes) || bytes == 0) { | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ class SessionController { | |||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var listeningSessions = [] |     let listeningSessions = [] | ||||||
|     if (req.query.user) { |     if (req.query.user) { | ||||||
|       listeningSessions = await this.getUserListeningSessionsHelper(req.query.user) |       listeningSessions = await this.getUserListeningSessionsHelper(req.query.user) | ||||||
|     } else { |     } else { | ||||||
| @ -42,6 +42,25 @@ class SessionController { | |||||||
|     res.json(payload) |     res.json(payload) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   getOpenSessions(req, res) { | ||||||
|  |     if (!req.user.isAdminOrUp) { | ||||||
|  |       Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`) | ||||||
|  |       return res.sendStatus(404) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const openSessions = this.playbackSessionManager.sessions.map(se => { | ||||||
|  |       const user = this.db.users.find(u => u.id === se.userId) || null | ||||||
|  |       return { | ||||||
|  |         ...se.toJSON(), | ||||||
|  |         user: user ? { id: user.id, username: user.username } : null | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     res.json({ | ||||||
|  |       sessions: openSessions | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getOpenSession(req, res) { |   getOpenSession(req, res) { | ||||||
|     var libraryItem = this.db.getLibraryItem(req.session.libraryItemId) |     var libraryItem = this.db.getLibraryItem(req.session.libraryItemId) | ||||||
|     var sessionForClient = req.session.toJSONForClient(libraryItem) |     var sessionForClient = req.session.toJSONForClient(libraryItem) | ||||||
|  | |||||||
| @ -14,7 +14,6 @@ const PlaybackSession = require('../objects/PlaybackSession') | |||||||
| const DeviceInfo = require('../objects/DeviceInfo') | const DeviceInfo = require('../objects/DeviceInfo') | ||||||
| const Stream = require('../objects/Stream') | const Stream = require('../objects/Stream') | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| class PlaybackSessionManager { | class PlaybackSessionManager { | ||||||
|   constructor(db) { |   constructor(db) { | ||||||
|     this.db = db |     this.db = db | ||||||
| @ -31,13 +30,14 @@ class PlaybackSessionManager { | |||||||
|   } |   } | ||||||
|   getStream(sessionId) { |   getStream(sessionId) { | ||||||
|     const session = this.getSession(sessionId) |     const session = this.getSession(sessionId) | ||||||
|     return session ? session.stream : null |     return session?.stream || null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getDeviceInfo(req) { |   getDeviceInfo(req) { | ||||||
|     const ua = uaParserJs(req.headers['user-agent']) |     const ua = uaParserJs(req.headers['user-agent']) | ||||||
|     const ip = requestIp.getClientIp(req) |     const ip = requestIp.getClientIp(req) | ||||||
|     const clientDeviceInfo = req.body ? req.body.deviceInfo || null : null // From mobile client
 | 
 | ||||||
|  |     const clientDeviceInfo = req.body?.deviceInfo || null | ||||||
| 
 | 
 | ||||||
|     const deviceInfo = new DeviceInfo() |     const deviceInfo = new DeviceInfo() | ||||||
|     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion) |     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion) | ||||||
| @ -138,18 +138,6 @@ class PlaybackSessionManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async syncLocalSessionRequest(user, sessionJson, res) { |   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 |  | ||||||
|       if (sessionJson.episodeId) return playbackSession.episodeId !== sessionJson.episodeId |  | ||||||
|       return playbackSession.libraryItemId === sessionJson.libraryItemId |  | ||||||
|     }) |  | ||||||
|     if (userSessionForThisItem) { |  | ||||||
|       Logger.info(`[PlaybackSessionManager] syncLocalSessionRequest: Closing open session "${userSessionForThisItem.displayTitle}" for user "${user.username}"`) |  | ||||||
|       await this.closeSession(user, userSessionForThisItem, null) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Sync
 |  | ||||||
|     const result = await this.syncLocalSession(user, sessionJson) |     const result = await this.syncLocalSession(user, sessionJson) | ||||||
|     if (result.error) { |     if (result.error) { | ||||||
|       res.status(500).send(result.error) |       res.status(500).send(result.error) | ||||||
| @ -164,8 +152,8 @@ class PlaybackSessionManager { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async startSession(user, deviceInfo, libraryItem, episodeId, options) { |   async startSession(user, deviceInfo, libraryItem, episodeId, options) { | ||||||
|     // Close any sessions already open for user
 |     // Close any sessions already open for user and device
 | ||||||
|     const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id) |     const userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id && playbackSession.deviceId === deviceInfo.deviceId) | ||||||
|     for (const session of userSessions) { |     for (const session of userSessions) { | ||||||
|       Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`) |       Logger.info(`[PlaybackSessionManager] startSession: Closing open session "${session.displayTitle}" for user "${user.username}" (Device: ${session.deviceDescription})`) | ||||||
|       await this.closeSession(user, session, null) |       await this.closeSession(user, session, null) | ||||||
| @ -268,6 +256,7 @@ class PlaybackSessionManager { | |||||||
|     } |     } | ||||||
|     Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`) |     Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`) | ||||||
|     SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems)) |     SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems)) | ||||||
|  |     SocketAuthority.clientEmitter(session.userId, 'user_session_closed', session.id) | ||||||
|     return this.removeSession(session.id) |     return this.removeSession(session.id) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| class DeviceInfo { | class DeviceInfo { | ||||||
|   constructor(deviceInfo = null) { |   constructor(deviceInfo = null) { | ||||||
|  |     this.deviceId = null | ||||||
|     this.ipAddress = null |     this.ipAddress = null | ||||||
| 
 | 
 | ||||||
|     // From User Agent (see: https://www.npmjs.com/package/ua-parser-js)
 |     // From User Agent (see: https://www.npmjs.com/package/ua-parser-js)
 | ||||||
| @ -32,6 +33,7 @@ class DeviceInfo { | |||||||
| 
 | 
 | ||||||
|   toJSON() { |   toJSON() { | ||||||
|     const obj = { |     const obj = { | ||||||
|  |       deviceId: this.deviceId, | ||||||
|       ipAddress: this.ipAddress, |       ipAddress: this.ipAddress, | ||||||
|       browserName: this.browserName, |       browserName: this.browserName, | ||||||
|       browserVersion: this.browserVersion, |       browserVersion: this.browserVersion, | ||||||
| @ -60,23 +62,42 @@ class DeviceInfo { | |||||||
|     return `${this.osName} ${this.osVersion} / ${this.browserName}` |     return `${this.osName} ${this.osVersion} / ${this.browserName}` | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // When client doesn't send a device id
 | ||||||
|  |   getTempDeviceId() { | ||||||
|  |     const keys = [ | ||||||
|  |       this.browserName, | ||||||
|  |       this.browserVersion, | ||||||
|  |       this.osName, | ||||||
|  |       this.osVersion, | ||||||
|  |       this.clientVersion, | ||||||
|  |       this.manufacturer, | ||||||
|  |       this.model, | ||||||
|  |       this.sdkVersion, | ||||||
|  |       this.ipAddress | ||||||
|  |     ].map(k => k || '') | ||||||
|  |     return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   setData(ip, ua, clientDeviceInfo, serverVersion) { |   setData(ip, ua, clientDeviceInfo, serverVersion) { | ||||||
|  |     this.deviceId = clientDeviceInfo?.deviceId || null | ||||||
|     this.ipAddress = ip || null |     this.ipAddress = ip || null | ||||||
| 
 | 
 | ||||||
|     const uaObj = ua || {} |     this.browserName = ua?.browser.name || null | ||||||
|     this.browserName = uaObj.browser.name || null |     this.browserVersion = ua?.browser.version || null | ||||||
|     this.browserVersion = uaObj.browser.version || null |     this.osName = ua?.os.name || null | ||||||
|     this.osName = uaObj.os.name || null |     this.osVersion = ua?.os.version || null | ||||||
|     this.osVersion = uaObj.os.version || null |     this.deviceType = ua?.device.type || null | ||||||
|     this.deviceType = uaObj.device.type || null |  | ||||||
| 
 | 
 | ||||||
|     const cdi = clientDeviceInfo || {} |     this.clientVersion = clientDeviceInfo?.clientVersion || null | ||||||
|     this.clientVersion = cdi.clientVersion || null |     this.manufacturer = clientDeviceInfo?.manufacturer || null | ||||||
|     this.manufacturer = cdi.manufacturer || null |     this.model = clientDeviceInfo?.model || null | ||||||
|     this.model = cdi.model || null |     this.sdkVersion = clientDeviceInfo?.sdkVersion || null | ||||||
|     this.sdkVersion = cdi.sdkVersion || null |  | ||||||
| 
 | 
 | ||||||
|     this.serverVersion = serverVersion || null |     this.serverVersion = serverVersion || null | ||||||
|  | 
 | ||||||
|  |     if (!this.deviceId) { | ||||||
|  |       this.deviceId = this.getTempDeviceId() | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| module.exports = DeviceInfo | module.exports = DeviceInfo | ||||||
| @ -55,7 +55,7 @@ class PlaybackSession { | |||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       episodeId: this.episodeId, |       episodeId: this.episodeId, | ||||||
|       mediaType: this.mediaType, |       mediaType: this.mediaType, | ||||||
|       mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null, |       mediaMetadata: this.mediaMetadata?.toJSON() || null, | ||||||
|       chapters: (this.chapters || []).map(c => ({ ...c })), |       chapters: (this.chapters || []).map(c => ({ ...c })), | ||||||
|       displayTitle: this.displayTitle, |       displayTitle: this.displayTitle, | ||||||
|       displayAuthor: this.displayAuthor, |       displayAuthor: this.displayAuthor, | ||||||
| @ -63,7 +63,7 @@ class PlaybackSession { | |||||||
|       duration: this.duration, |       duration: this.duration, | ||||||
|       playMethod: this.playMethod, |       playMethod: this.playMethod, | ||||||
|       mediaPlayer: this.mediaPlayer, |       mediaPlayer: this.mediaPlayer, | ||||||
|       deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null, |       deviceInfo: this.deviceInfo?.toJSON() || null, | ||||||
|       date: this.date, |       date: this.date, | ||||||
|       dayOfWeek: this.dayOfWeek, |       dayOfWeek: this.dayOfWeek, | ||||||
|       timeListening: this.timeListening, |       timeListening: this.timeListening, | ||||||
| @ -82,7 +82,7 @@ class PlaybackSession { | |||||||
|       libraryItemId: this.libraryItemId, |       libraryItemId: this.libraryItemId, | ||||||
|       episodeId: this.episodeId, |       episodeId: this.episodeId, | ||||||
|       mediaType: this.mediaType, |       mediaType: this.mediaType, | ||||||
|       mediaMetadata: this.mediaMetadata ? this.mediaMetadata.toJSON() : null, |       mediaMetadata: this.mediaMetadata?.toJSON() || null, | ||||||
|       chapters: (this.chapters || []).map(c => ({ ...c })), |       chapters: (this.chapters || []).map(c => ({ ...c })), | ||||||
|       displayTitle: this.displayTitle, |       displayTitle: this.displayTitle, | ||||||
|       displayAuthor: this.displayAuthor, |       displayAuthor: this.displayAuthor, | ||||||
| @ -90,7 +90,7 @@ class PlaybackSession { | |||||||
|       duration: this.duration, |       duration: this.duration, | ||||||
|       playMethod: this.playMethod, |       playMethod: this.playMethod, | ||||||
|       mediaPlayer: this.mediaPlayer, |       mediaPlayer: this.mediaPlayer, | ||||||
|       deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null, |       deviceInfo: this.deviceInfo?.toJSON() || null, | ||||||
|       date: this.date, |       date: this.date, | ||||||
|       dayOfWeek: this.dayOfWeek, |       dayOfWeek: this.dayOfWeek, | ||||||
|       timeListening: this.timeListening, |       timeListening: this.timeListening, | ||||||
| @ -151,6 +151,10 @@ class PlaybackSession { | |||||||
|     return Math.max(0, Math.min(this.currentTime / this.duration, 1)) |     return Math.max(0, Math.min(this.currentTime / this.duration, 1)) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   get deviceId() { | ||||||
|  |     return this.deviceInfo?.deviceId | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   get deviceDescription() { |   get deviceDescription() { | ||||||
|     if (!this.deviceInfo) return 'No Device Info' |     if (!this.deviceInfo) return 'No Device Info' | ||||||
|     return this.deviceInfo.deviceDescription |     return this.deviceInfo.deviceDescription | ||||||
|  | |||||||
| @ -214,6 +214,7 @@ 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.get('/sessions/open', SessionController.getOpenSessions.bind(this)) | ||||||
|     this.router.post('/session/local', SessionController.syncLocal.bind(this)) |     this.router.post('/session/local', SessionController.syncLocal.bind(this)) | ||||||
|     this.router.post('/session/local-all', SessionController.syncLocalSessions.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
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user