mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:User listening sessions page, Update:Listening sessions to save media times and device info
This commit is contained in:
		
							parent
							
								
									54663f0f01
								
							
						
					
					
						commit
						f002532c1e
					
				| @ -32,6 +32,7 @@ export default { | ||||
|       default: '' | ||||
|     }, | ||||
|     paddingX: Number, | ||||
|     paddingY: Number, | ||||
|     small: Boolean, | ||||
|     loading: Boolean, | ||||
|     disabled: Boolean | ||||
| @ -48,14 +49,17 @@ export default { | ||||
|       if (this.small) { | ||||
|         list.push('text-sm') | ||||
|         if (this.paddingX === undefined) list.push('px-4') | ||||
|         list.push('py-1') | ||||
|         if (this.paddingY === undefined) list.push('py-1') | ||||
|       } else { | ||||
|         if (this.paddingX === undefined) list.push('px-8') | ||||
|         list.push('py-2') | ||||
|         if (this.paddingY === undefined) list.push('py-2') | ||||
|       } | ||||
|       if (this.paddingX !== undefined) { | ||||
|         list.push(`px-${this.paddingX}`) | ||||
|       } | ||||
|       if (this.paddingY !== undefined) { | ||||
|         list.push(`py-${this.paddingY}`) | ||||
|       } | ||||
|       if (this.disabled) { | ||||
|         list.push('cursor-not-allowed') | ||||
|       } | ||||
|  | ||||
| @ -22,7 +22,10 @@ | ||||
|       <div class="w-full h-px bg-white bg-opacity-10 my-2" /> | ||||
|       <div class="py-2"> | ||||
|         <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats</h1> | ||||
|         <p class="text-sm text-gray-300">{{ listeningSessions.length }} Listening Sessions</p> | ||||
|         <div class="flex items-center"> | ||||
|           <p class="text-sm text-gray-300">{{ listeningSessions.length }} Listening Sessions</p> | ||||
|           <ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs mx-2" :padding-x="1.5" :padding-y="1">View All</ui-btn> | ||||
|         </div> | ||||
|         <p class="text-sm text-gray-300"> | ||||
|           Total Time Listened:  | ||||
|           <span class="font-mono text-base">{{ listeningTimePretty }}</span> | ||||
| @ -35,7 +38,7 @@ | ||||
|         <div v-if="latestSession" class="mt-4"> | ||||
|           <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Last Listening Session</h1> | ||||
|           <p class="text-sm text-gray-300"> | ||||
|             <strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for  <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span> | ||||
|             <strong>{{ latestSession.displayTitle }}</strong> {{ $dateDistanceFromNow(latestSession.updatedAt) }} for <span class="font-mono text-base">{{ $elapsedPrettyExtended(this.latestSession.timeListening) }}</span> | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -73,7 +76,7 @@ | ||||
|             </td> | ||||
|           </tr> | ||||
|         </table> | ||||
|         <p v-else class="text-white text-opacity-50">Nothing read yet...</p> | ||||
|         <p v-else class="text-white text-opacity-50">Nothing listened to yet...</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
							
								
								
									
										146
									
								
								client/pages/config/users/_id/sessions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								client/pages/config/users/_id/sessions.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | ||||
| <template> | ||||
|   <div class="w-full h-full"> | ||||
|     <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8"> | ||||
|       <nuxt-link :to="`/config/users/${user.id}`" class="text-white text-opacity-70 hover:text-opacity-100 hover:bg-white hover:bg-opacity-5 cursor-pointer rounded-full px-2 sm:px-0"> | ||||
|         <div class="flex items-center"> | ||||
|           <div class="h-10 w-10 flex items-center justify-center"> | ||||
|             <span class="material-icons text-2xl">arrow_back</span> | ||||
|           </div> | ||||
|           <p class="pl-1">Back to User</p> | ||||
|         </div> | ||||
|       </nuxt-link> | ||||
|       <div class="flex items-center mb-2 mt-4 px-2 sm:px-0"> | ||||
|         <widgets-online-indicator :value="!!userOnline" /> | ||||
|         <h1 class="text-xl pl-2">{{ username }}</h1> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="w-full h-px bg-white bg-opacity-10 my-2" /> | ||||
| 
 | ||||
|       <div class="py-2"> | ||||
|         <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1> | ||||
|         <table v-if="listeningSessions.length" class="userSessionsTable"> | ||||
|           <tr class="bg-primary bg-opacity-40"> | ||||
|             <th class="flex-grow text-left">Item</th> | ||||
|             <th class="w-40 text-left hidden md:table-cell">Play Method</th> | ||||
|             <th class="w-40 text-left hidden sm:table-cell">Device Info</th> | ||||
|             <th class="w-20">Listening Time</th> | ||||
|             <th class="w-20">Last Time</th> | ||||
|             <!-- <th class="w-40 hidden sm:table-cell">Started At</th> --> | ||||
|             <th class="w-40 hidden sm:table-cell">Last Update</th> | ||||
|           </tr> | ||||
|           <tr v-for="session in listeningSessions" :key="session.id"> | ||||
|             <td class="py-1"> | ||||
|               <p class="text-sm text-gray-200">{{ session.displayTitle }}</p> | ||||
|               <p class="text-xs text-gray-400">{{ session.displayAuthor }}</p> | ||||
|             </td> | ||||
|             <td class="hidden md:table-cell"> | ||||
|               <p class="text-xs">{{ getPlayMethodName(session.playMethod) }} with {{ session.mediaPlayer }}</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"> | ||||
|               <p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p> | ||||
|             </td> | ||||
|             <!-- <td class="text-center hidden sm:table-cell"> | ||||
|               <ui-tooltip v-if="session.startedAt" direction="top" :text="$formatDate(session.startedAt, 'MMMM do, yyyy HH:mm')"> | ||||
|                 <p class="text-xs">{{ $dateDistanceFromNow(session.startedAt) }}</p> | ||||
|               </ui-tooltip> | ||||
|             </td> --> | ||||
|             <td class="text-center hidden sm:table-cell"> | ||||
|               <ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')"> | ||||
|                 <p class="text-xs">{{ $dateDistanceFromNow(session.updatedAt) }}</p> | ||||
|               </ui-tooltip> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </table> | ||||
|         <p v-else class="text-white text-opacity-50">No sessions yet...</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   async asyncData({ params, redirect, app }) { | ||||
|     var user = await app.$axios.$get(`/api/users/${params.id}`).catch((error) => { | ||||
|       console.error('Failed to get user', error) | ||||
|       return null | ||||
|     }) | ||||
|     if (!user) return redirect('/config/users') | ||||
|     return { | ||||
|       user | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       listeningSessions: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     username() { | ||||
|       return this.user.username | ||||
|     }, | ||||
|     userOnline() { | ||||
|       return this.$store.getters['users/getIsUserOnline'](this.user.id) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getDeviceInfoString(deviceInfo) { | ||||
|       if (!deviceInfo) return '' | ||||
|       var lines = [] | ||||
|       if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`) | ||||
|       if (deviceInfo.browserName) lines.push(deviceInfo.browserName) | ||||
| 
 | ||||
|       if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`) | ||||
|       if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`) | ||||
|       return lines.join('<br>') | ||||
|     }, | ||||
|     getPlayMethodName(playMethod) { | ||||
|       if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' | ||||
|       else if (playMethod === this.$constants.PlayMethod.TRANSCODE) return 'Transcode' | ||||
|       else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream' | ||||
|       else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local' | ||||
|       return 'Unknown' | ||||
|     }, | ||||
|     async init() { | ||||
|       console.log(navigator) | ||||
| 
 | ||||
|       this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => { | ||||
|         console.error('Failed to load listening sesions', err) | ||||
|         return [] | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| .userSessionsTable { | ||||
|   border-collapse: collapse; | ||||
|   width: 100%; | ||||
|   border: 1px solid #474747; | ||||
| } | ||||
| .userSessionsTable tr:nth-child(even) { | ||||
|   background-color: #2e2e2e; | ||||
| } | ||||
| .userSessionsTable tr:not(:first-child) { | ||||
|   background-color: #373838; | ||||
| } | ||||
| .userSessionsTable tr:hover:not(:first-child) { | ||||
|   background-color: #474747; | ||||
| } | ||||
| .userSessionsTable td { | ||||
|   padding: 4px 8px; | ||||
| } | ||||
| .userSessionsTable th { | ||||
|   padding: 4px 8px; | ||||
|   font-size: 0.75rem; | ||||
| } | ||||
| </style> | ||||
| @ -28,7 +28,8 @@ const BookshelfView = { | ||||
| const PlayMethod = { | ||||
|   DIRECTPLAY: 0, | ||||
|   DIRECTSTREAM: 1, | ||||
|   TRANSCODE: 2 | ||||
|   TRANSCODE: 2, | ||||
|   LOCAL: 3 | ||||
| } | ||||
| 
 | ||||
| const Constants = { | ||||
|  | ||||
| @ -57,6 +57,7 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => { | ||||
| } | ||||
| 
 | ||||
| Vue.prototype.$secondsToTimestamp = (seconds) => { | ||||
|   if (!seconds) return '0:00' | ||||
|   var _seconds = seconds | ||||
|   var _minutes = Math.floor(seconds / 60) | ||||
|   _seconds -= _minutes * 60 | ||||
|  | ||||
| @ -189,8 +189,8 @@ class LibraryItemController { | ||||
|       Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     const options = req.body || {} | ||||
|     this.playbackSessionManager.startSessionRequest(req.user, req.libraryItem, null, options, res) | ||||
| 
 | ||||
|     this.playbackSessionManager.startSessionRequest(req, res, null) | ||||
|   } | ||||
| 
 | ||||
|   // POST: api/items/:id/play/:episodeId
 | ||||
| @ -206,8 +206,7 @@ class LibraryItemController { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
| 
 | ||||
|     const options = req.body || {} | ||||
|     this.playbackSessionManager.startSessionRequest(req.user, libraryItem, episodeId, options, res) | ||||
|     this.playbackSessionManager.startSessionRequest(req, res, episodeId) | ||||
|   } | ||||
| 
 | ||||
|   // PATCH: api/items/:id/tracks
 | ||||
|  | ||||
							
								
								
									
										5
									
								
								server/libs/isJs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								server/libs/isJs.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										174
									
								
								server/libs/requestIp.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								server/libs/requestIp.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | ||||
| // SOURCE: https://github.com/pbojinov/request-ip
 | ||||
| 
 | ||||
| "use strict"; | ||||
| 
 | ||||
| function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } | ||||
| 
 | ||||
| var is = require('./isJs'); | ||||
| /** | ||||
|  * Parse x-forwarded-for headers. | ||||
|  * | ||||
|  * @param {string} value - The value to be parsed. | ||||
|  * @return {string|null} First known IP address, if any. | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| function getClientIpFromXForwardedFor(value) { | ||||
|   if (!is.existy(value)) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   if (is.not.string(value)) { | ||||
|     throw new TypeError("Expected a string, got \"".concat(_typeof(value), "\"")); | ||||
|   } // x-forwarded-for may return multiple IP addresses in the format:
 | ||||
|   // "client IP, proxy 1 IP, proxy 2 IP"
 | ||||
|   // Therefore, the right-most IP address is the IP address of the most recent proxy
 | ||||
|   // and the left-most IP address is the IP address of the originating client.
 | ||||
|   // source: http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html
 | ||||
|   // Azure Web App's also adds a port for some reason, so we'll only use the first part (the IP)
 | ||||
| 
 | ||||
| 
 | ||||
|   var forwardedIps = value.split(',').map(function (e) { | ||||
|     var ip = e.trim(); | ||||
| 
 | ||||
|     if (ip.includes(':')) { | ||||
|       var splitted = ip.split(':'); // make sure we only use this if it's ipv4 (ip:port)
 | ||||
| 
 | ||||
|       if (splitted.length === 2) { | ||||
|         return splitted[0]; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return ip; | ||||
|   }); // Sometimes IP addresses in this header can be 'unknown' (http://stackoverflow.com/a/11285650).
 | ||||
|   // Therefore taking the left-most IP address that is not unknown
 | ||||
|   // A Squid configuration directive can also set the value to "unknown" (http://www.squid-cache.org/Doc/config/forwarded_for/)
 | ||||
| 
 | ||||
|   return forwardedIps.find(is.ip); | ||||
| } | ||||
| /** | ||||
|  * Determine client IP address. | ||||
|  * | ||||
|  * @param req | ||||
|  * @returns {string} ip - The IP address if known, defaulting to empty string if unknown. | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| function getClientIp(req) { | ||||
|   // Server is probably behind a proxy.
 | ||||
|   if (req.headers) { | ||||
|     // Standard headers used by Amazon EC2, Heroku, and others.
 | ||||
|     if (is.ip(req.headers['x-client-ip'])) { | ||||
|       return req.headers['x-client-ip']; | ||||
|     } // Load-balancers (AWS ELB) or proxies.
 | ||||
| 
 | ||||
| 
 | ||||
|     var xForwardedFor = getClientIpFromXForwardedFor(req.headers['x-forwarded-for']); | ||||
| 
 | ||||
|     if (is.ip(xForwardedFor)) { | ||||
|       return xForwardedFor; | ||||
|     } // Cloudflare.
 | ||||
|     // @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
 | ||||
|     // CF-Connecting-IP - applied to every request to the origin.
 | ||||
| 
 | ||||
| 
 | ||||
|     if (is.ip(req.headers['cf-connecting-ip'])) { | ||||
|       return req.headers['cf-connecting-ip']; | ||||
|     } // Fastly and Firebase hosting header (When forwared to cloud function)
 | ||||
| 
 | ||||
| 
 | ||||
|     if (is.ip(req.headers['fastly-client-ip'])) { | ||||
|       return req.headers['fastly-client-ip']; | ||||
|     } // Akamai and Cloudflare: True-Client-IP.
 | ||||
| 
 | ||||
| 
 | ||||
|     if (is.ip(req.headers['true-client-ip'])) { | ||||
|       return req.headers['true-client-ip']; | ||||
|     } // Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies.
 | ||||
| 
 | ||||
| 
 | ||||
|     if (is.ip(req.headers['x-real-ip'])) { | ||||
|       return req.headers['x-real-ip']; | ||||
|     } // (Rackspace LB and Riverbed's Stingray)
 | ||||
|     // http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address
 | ||||
|     // https://splash.riverbed.com/docs/DOC-1926
 | ||||
| 
 | ||||
| 
 | ||||
|     if (is.ip(req.headers['x-cluster-client-ip'])) { | ||||
|       return req.headers['x-cluster-client-ip']; | ||||
|     } | ||||
| 
 | ||||
|     if (is.ip(req.headers['x-forwarded'])) { | ||||
|       return req.headers['x-forwarded']; | ||||
|     } | ||||
| 
 | ||||
|     if (is.ip(req.headers['forwarded-for'])) { | ||||
|       return req.headers['forwarded-for']; | ||||
|     } | ||||
| 
 | ||||
|     if (is.ip(req.headers.forwarded)) { | ||||
|       return req.headers.forwarded; | ||||
|     } | ||||
|   } // Remote address checks.
 | ||||
| 
 | ||||
| 
 | ||||
|   if (is.existy(req.connection)) { | ||||
|     if (is.ip(req.connection.remoteAddress)) { | ||||
|       return req.connection.remoteAddress; | ||||
|     } | ||||
| 
 | ||||
|     if (is.existy(req.connection.socket) && is.ip(req.connection.socket.remoteAddress)) { | ||||
|       return req.connection.socket.remoteAddress; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) { | ||||
|     return req.socket.remoteAddress; | ||||
|   } | ||||
| 
 | ||||
|   if (is.existy(req.info) && is.ip(req.info.remoteAddress)) { | ||||
|     return req.info.remoteAddress; | ||||
|   } // AWS Api Gateway + Lambda
 | ||||
| 
 | ||||
| 
 | ||||
|   if (is.existy(req.requestContext) && is.existy(req.requestContext.identity) && is.ip(req.requestContext.identity.sourceIp)) { | ||||
|     return req.requestContext.identity.sourceIp; | ||||
|   } | ||||
| 
 | ||||
|   return null; | ||||
| } | ||||
| /** | ||||
|  * Expose request IP as a middleware. | ||||
|  * | ||||
|  * @param {object} [options] - Configuration. | ||||
|  * @param {string} [options.attributeName] - Name of attribute to augment request object with. | ||||
|  * @return {*} | ||||
|  */ | ||||
| 
 | ||||
| 
 | ||||
| function mw(options) { | ||||
|   // Defaults.
 | ||||
|   var configuration = is.not.existy(options) ? {} : options; // Validation.
 | ||||
| 
 | ||||
|   if (is.not.object(configuration)) { | ||||
|     throw new TypeError('Options must be an object!'); | ||||
|   } | ||||
| 
 | ||||
|   var attributeName = configuration.attributeName || 'clientIp'; | ||||
|   return function (req, res, next) { | ||||
|     var ip = getClientIp(req); | ||||
|     Object.defineProperty(req, attributeName, { | ||||
|       get: function get() { | ||||
|         return ip; | ||||
|       }, | ||||
|       configurable: true | ||||
|     }); | ||||
|     next(); | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|   getClientIpFromXForwardedFor: getClientIpFromXForwardedFor, | ||||
|   getClientIp: getClientIp, | ||||
|   mw: mw | ||||
| }; | ||||
							
								
								
									
										4
									
								
								server/libs/uaParserJs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								server/libs/uaParserJs.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -1,11 +1,16 @@ | ||||
| const Path = require('path') | ||||
| const date = require('date-and-time') | ||||
| const serverVersion = require('../../package.json').version | ||||
| const { PlayMethod } = require('../utils/constants') | ||||
| const PlaybackSession = require('../objects/PlaybackSession') | ||||
| const DeviceInfo = require('../objects/DeviceInfo') | ||||
| const Stream = require('../objects/Stream') | ||||
| const Logger = require('../Logger') | ||||
| const fs = require('fs-extra') | ||||
| 
 | ||||
| const uaParserJs = require('../libs/uaParserJs') | ||||
| const requestIp = require('../libs/requestIp') | ||||
| 
 | ||||
| class PlaybackSessionManager { | ||||
|   constructor(db, emitter, clientEmitter) { | ||||
|     this.db = db | ||||
| @ -27,8 +32,21 @@ class PlaybackSessionManager { | ||||
|     return session ? session.stream : null | ||||
|   } | ||||
| 
 | ||||
|   async startSessionRequest(user, libraryItem, episodeId, options, res) { | ||||
|     const session = await this.startSession(user, libraryItem, episodeId, options) | ||||
|   getDeviceInfo(req) { | ||||
|     const ua = uaParserJs(req.headers['user-agent']) | ||||
|     const ip = requestIp.getClientIp(req) | ||||
|     const clientDeviceInfo = req.body ? req.body.deviceInfo || null : null // From mobile client
 | ||||
| 
 | ||||
|     const deviceInfo = new DeviceInfo() | ||||
|     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion) | ||||
|     return deviceInfo | ||||
|   } | ||||
| 
 | ||||
|   async startSessionRequest(req, res, episodeId) { | ||||
|     const deviceInfo = this.getDeviceInfo(req) | ||||
| 
 | ||||
|     const { user, libraryItem, body: options } = req | ||||
|     const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) | ||||
|     res.json(session.toJSONForClient(libraryItem)) | ||||
|   } | ||||
| 
 | ||||
| @ -84,7 +102,7 @@ class PlaybackSessionManager { | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async startSession(user, libraryItem, episodeId, options) { | ||||
|   async startSession(user, deviceInfo, libraryItem, episodeId, options) { | ||||
|     // Close any sessions already open for user
 | ||||
|     var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id) | ||||
|     for (const session of userSessions) { | ||||
| @ -99,7 +117,7 @@ class PlaybackSessionManager { | ||||
|     var userStartTime = 0 | ||||
|     if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0 | ||||
|     const newPlaybackSession = new PlaybackSession() | ||||
|     newPlaybackSession.setData(libraryItem, user, mediaPlayer, episodeId) | ||||
|     newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId) | ||||
| 
 | ||||
|     var audioTracks = [] | ||||
|     if (shouldDirectPlay) { | ||||
| @ -122,7 +140,6 @@ class PlaybackSessionManager { | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     newPlaybackSession.currentTime = userStartTime | ||||
|     newPlaybackSession.audioTracks = audioTracks | ||||
| 
 | ||||
|     // Will save on the first sync
 | ||||
|  | ||||
							
								
								
									
										74
									
								
								server/objects/DeviceInfo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/objects/DeviceInfo.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | ||||
| class DeviceInfo { | ||||
|   constructor(deviceInfo = null) { | ||||
|     this.ipAddress = null | ||||
| 
 | ||||
|     // From User Agent (see: https://www.npmjs.com/package/ua-parser-js)
 | ||||
|     this.browserName = null | ||||
|     this.browserVersion = null | ||||
|     this.osName = null | ||||
|     this.osVersion = null | ||||
|     this.deviceType = null | ||||
| 
 | ||||
|     // From client
 | ||||
|     this.clientVersion = null | ||||
|     this.manufacturer = null | ||||
|     this.model = null | ||||
|     this.sdkVersion = null // Android Only
 | ||||
| 
 | ||||
|     this.serverVersion = null | ||||
| 
 | ||||
|     if (deviceInfo) { | ||||
|       this.construct(deviceInfo) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   construct(deviceInfo) { | ||||
|     for (const key in deviceInfo) { | ||||
|       if (deviceInfo[key] !== undefined && this[key] !== undefined) { | ||||
|         this[key] = deviceInfo[key] | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
|     const obj = { | ||||
|       ipAddress: this.ipAddress, | ||||
|       browserName: this.browserName, | ||||
|       browserVersion: this.browserVersion, | ||||
|       osName: this.osName, | ||||
|       osVersion: this.osVersion, | ||||
|       deviceType: this.deviceType, | ||||
|       clientVersion: this.clientVersion, | ||||
|       manufacturer: this.manufacturer, | ||||
|       model: this.model, | ||||
|       sdkVersion: this.sdkVersion, | ||||
|       serverVersion: this.serverVersion | ||||
|     } | ||||
|     for (const key in obj) { | ||||
|       if (obj[key] === null || obj[key] === undefined) { | ||||
|         delete obj[key] | ||||
|       } | ||||
|     } | ||||
|     return obj | ||||
|   } | ||||
| 
 | ||||
|   setData(ip, ua, clientDeviceInfo, serverVersion) { | ||||
|     this.ipAddress = ip || null | ||||
| 
 | ||||
|     const uaObj = ua || {} | ||||
|     this.browserName = uaObj.browser.name || null | ||||
|     this.browserVersion = uaObj.browser.version || null | ||||
|     this.osName = uaObj.os.name || null | ||||
|     this.osVersion = uaObj.os.version || null | ||||
|     this.deviceType = uaObj.device.type || null | ||||
| 
 | ||||
|     var cdi = clientDeviceInfo || {} | ||||
|     this.clientVersion = cdi.clientVersion || null | ||||
|     this.manufacturer = cdi.manufacturer || null | ||||
|     this.model = cdi.model || null | ||||
|     this.sdkVersion = cdi.sdkVersion || null | ||||
| 
 | ||||
|     this.serverVersion = serverVersion || null | ||||
|   } | ||||
| } | ||||
| module.exports = DeviceInfo | ||||
| @ -3,6 +3,7 @@ const { getId } = require('../utils/index') | ||||
| const { PlayMethod } = require('../utils/constants') | ||||
| const BookMetadata = require('./metadata/BookMetadata') | ||||
| const PodcastMetadata = require('./metadata/PodcastMetadata') | ||||
| const DeviceInfo = require('./DeviceInfo') | ||||
| 
 | ||||
| class PlaybackSession { | ||||
|   constructor(session) { | ||||
| @ -21,18 +22,21 @@ class PlaybackSession { | ||||
| 
 | ||||
|     this.playMethod = null | ||||
|     this.mediaPlayer = null | ||||
|     this.deviceInfo = null | ||||
| 
 | ||||
|     this.date = null | ||||
|     this.dayOfWeek = null | ||||
| 
 | ||||
|     this.timeListening = null | ||||
|     this.startTime = null // media current time at start of playback
 | ||||
|     this.currentTime = 0 // Last current time set
 | ||||
| 
 | ||||
|     this.startedAt = null | ||||
|     this.updatedAt = null | ||||
| 
 | ||||
|     // Not saved in DB
 | ||||
|     this.lastSave = 0 | ||||
|     this.audioTracks = [] | ||||
|     this.currentTime = 0 | ||||
|     this.stream = null | ||||
| 
 | ||||
|     if (session) { | ||||
| @ -56,10 +60,13 @@ class PlaybackSession { | ||||
|       duration: this.duration, | ||||
|       playMethod: this.playMethod, | ||||
|       mediaPlayer: this.mediaPlayer, | ||||
|       deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null, | ||||
|       date: this.date, | ||||
|       dayOfWeek: this.dayOfWeek, | ||||
|       timeListening: this.timeListening, | ||||
|       lastUpdate: this.lastUpdate, | ||||
|       startTime: this.startTime, | ||||
|       currentTime: this.currentTime, | ||||
|       startedAt: this.startedAt, | ||||
|       updatedAt: this.updatedAt | ||||
|     } | ||||
|   } | ||||
| @ -80,13 +87,15 @@ class PlaybackSession { | ||||
|       duration: this.duration, | ||||
|       playMethod: this.playMethod, | ||||
|       mediaPlayer: this.mediaPlayer, | ||||
|       deviceInfo: this.deviceInfo ? this.deviceInfo.toJSON() : null, | ||||
|       date: this.date, | ||||
|       dayOfWeek: this.dayOfWeek, | ||||
|       timeListening: this.timeListening, | ||||
|       lastUpdate: this.lastUpdate, | ||||
|       startTime: this.startTime, | ||||
|       currentTime: this.currentTime, | ||||
|       startedAt: this.startedAt, | ||||
|       updatedAt: this.updatedAt, | ||||
|       audioTracks: this.audioTracks.map(at => at.toJSON()), | ||||
|       currentTime: this.currentTime, | ||||
|       libraryItem: libraryItem.toJSONExpanded() | ||||
|     } | ||||
|   } | ||||
| @ -101,6 +110,7 @@ class PlaybackSession { | ||||
|     this.duration = session.duration | ||||
|     this.playMethod = session.playMethod | ||||
|     this.mediaPlayer = session.mediaPlayer || null | ||||
|     this.deviceInfo = new DeviceInfo(session.deviceInfo) | ||||
|     this.chapters = session.chapters || [] | ||||
| 
 | ||||
|     this.mediaMetadata = null | ||||
| @ -118,6 +128,9 @@ class PlaybackSession { | ||||
|     this.dayOfWeek = session.dayOfWeek | ||||
| 
 | ||||
|     this.timeListening = session.timeListening || null | ||||
|     this.startTime = session.startTime || 0 | ||||
|     this.currentTime = session.currentTime || 0 | ||||
| 
 | ||||
|     this.startedAt = session.startedAt | ||||
|     this.updatedAt = session.updatedAt || null | ||||
|   } | ||||
| @ -127,7 +140,7 @@ class PlaybackSession { | ||||
|     return Math.max(0, Math.min(this.currentTime / this.duration, 1)) | ||||
|   } | ||||
| 
 | ||||
|   setData(libraryItem, user, mediaPlayer, episodeId = null) { | ||||
|   setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) { | ||||
|     this.id = getId('play') | ||||
|     this.userId = user.id | ||||
|     this.libraryItemId = libraryItem.id | ||||
| @ -146,8 +159,13 @@ class PlaybackSession { | ||||
|     } | ||||
| 
 | ||||
|     this.mediaPlayer = mediaPlayer | ||||
|     this.deviceInfo = deviceInfo || new DeviceInfo() | ||||
| 
 | ||||
| 
 | ||||
|     this.timeListening = 0 | ||||
|     this.startTime = startTime | ||||
|     this.currentTime = startTime | ||||
| 
 | ||||
|     this.date = date.format(new Date(), 'YYYY-MM-DD') | ||||
|     this.dayOfWeek = date.format(new Date(), 'dddd') | ||||
|     this.startedAt = Date.now() | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user