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: '' |       default: '' | ||||||
|     }, |     }, | ||||||
|     paddingX: Number, |     paddingX: Number, | ||||||
|  |     paddingY: Number, | ||||||
|     small: Boolean, |     small: Boolean, | ||||||
|     loading: Boolean, |     loading: Boolean, | ||||||
|     disabled: Boolean |     disabled: Boolean | ||||||
| @ -48,14 +49,17 @@ export default { | |||||||
|       if (this.small) { |       if (this.small) { | ||||||
|         list.push('text-sm') |         list.push('text-sm') | ||||||
|         if (this.paddingX === undefined) list.push('px-4') |         if (this.paddingX === undefined) list.push('px-4') | ||||||
|         list.push('py-1') |         if (this.paddingY === undefined) list.push('py-1') | ||||||
|       } else { |       } else { | ||||||
|         if (this.paddingX === undefined) list.push('px-8') |         if (this.paddingX === undefined) list.push('px-8') | ||||||
|         list.push('py-2') |         if (this.paddingY === undefined) list.push('py-2') | ||||||
|       } |       } | ||||||
|       if (this.paddingX !== undefined) { |       if (this.paddingX !== undefined) { | ||||||
|         list.push(`px-${this.paddingX}`) |         list.push(`px-${this.paddingX}`) | ||||||
|       } |       } | ||||||
|  |       if (this.paddingY !== undefined) { | ||||||
|  |         list.push(`py-${this.paddingY}`) | ||||||
|  |       } | ||||||
|       if (this.disabled) { |       if (this.disabled) { | ||||||
|         list.push('cursor-not-allowed') |         list.push('cursor-not-allowed') | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -22,7 +22,10 @@ | |||||||
|       <div class="w-full h-px bg-white bg-opacity-10 my-2" /> |       <div class="w-full h-px bg-white bg-opacity-10 my-2" /> | ||||||
|       <div class="py-2"> |       <div class="py-2"> | ||||||
|         <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Stats</h1> |         <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"> |         <p class="text-sm text-gray-300"> | ||||||
|           Total Time Listened:  |           Total Time Listened:  | ||||||
|           <span class="font-mono text-base">{{ listeningTimePretty }}</span> |           <span class="font-mono text-base">{{ listeningTimePretty }}</span> | ||||||
| @ -35,7 +38,7 @@ | |||||||
|         <div v-if="latestSession" class="mt-4"> |         <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> |           <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"> |           <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> |           </p> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -73,7 +76,7 @@ | |||||||
|             </td> |             </td> | ||||||
|           </tr> |           </tr> | ||||||
|         </table> |         </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> |     </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 = { | const PlayMethod = { | ||||||
|   DIRECTPLAY: 0, |   DIRECTPLAY: 0, | ||||||
|   DIRECTSTREAM: 1, |   DIRECTSTREAM: 1, | ||||||
|   TRANSCODE: 2 |   TRANSCODE: 2, | ||||||
|  |   LOCAL: 3 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Constants = { | const Constants = { | ||||||
|  | |||||||
| @ -57,6 +57,7 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Vue.prototype.$secondsToTimestamp = (seconds) => { | Vue.prototype.$secondsToTimestamp = (seconds) => { | ||||||
|  |   if (!seconds) return '0:00' | ||||||
|   var _seconds = seconds |   var _seconds = seconds | ||||||
|   var _minutes = Math.floor(seconds / 60) |   var _minutes = Math.floor(seconds / 60) | ||||||
|   _seconds -= _minutes * 60 |   _seconds -= _minutes * 60 | ||||||
|  | |||||||
| @ -189,8 +189,8 @@ class LibraryItemController { | |||||||
|       Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) |       Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) | ||||||
|       return res.sendStatus(404) |       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
 |   // POST: api/items/:id/play/:episodeId
 | ||||||
| @ -206,8 +206,7 @@ class LibraryItemController { | |||||||
|       return res.sendStatus(404) |       return res.sendStatus(404) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const options = req.body || {} |     this.playbackSessionManager.startSessionRequest(req, res, episodeId) | ||||||
|     this.playbackSessionManager.startSessionRequest(req.user, libraryItem, episodeId, options, res) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // PATCH: api/items/:id/tracks
 |   // 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 Path = require('path') | ||||||
| const date = require('date-and-time') | const date = require('date-and-time') | ||||||
|  | const serverVersion = require('../../package.json').version | ||||||
| const { PlayMethod } = require('../utils/constants') | const { PlayMethod } = require('../utils/constants') | ||||||
| const PlaybackSession = require('../objects/PlaybackSession') | const PlaybackSession = require('../objects/PlaybackSession') | ||||||
|  | const DeviceInfo = require('../objects/DeviceInfo') | ||||||
| const Stream = require('../objects/Stream') | const Stream = require('../objects/Stream') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const fs = require('fs-extra') | const fs = require('fs-extra') | ||||||
| 
 | 
 | ||||||
|  | const uaParserJs = require('../libs/uaParserJs') | ||||||
|  | const requestIp = require('../libs/requestIp') | ||||||
|  | 
 | ||||||
| class PlaybackSessionManager { | class PlaybackSessionManager { | ||||||
|   constructor(db, emitter, clientEmitter) { |   constructor(db, emitter, clientEmitter) { | ||||||
|     this.db = db |     this.db = db | ||||||
| @ -27,8 +32,21 @@ class PlaybackSessionManager { | |||||||
|     return session ? session.stream : null |     return session ? session.stream : null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async startSessionRequest(user, libraryItem, episodeId, options, res) { |   getDeviceInfo(req) { | ||||||
|     const session = await this.startSession(user, libraryItem, episodeId, options) |     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)) |     res.json(session.toJSONForClient(libraryItem)) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -84,7 +102,7 @@ class PlaybackSessionManager { | |||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async startSession(user, libraryItem, episodeId, options) { |   async startSession(user, deviceInfo, libraryItem, episodeId, options) { | ||||||
|     // Close any sessions already open for user
 |     // Close any sessions already open for user
 | ||||||
|     var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id) |     var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id) | ||||||
|     for (const session of userSessions) { |     for (const session of userSessions) { | ||||||
| @ -99,7 +117,7 @@ class PlaybackSessionManager { | |||||||
|     var userStartTime = 0 |     var userStartTime = 0 | ||||||
|     if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0 |     if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0 | ||||||
|     const newPlaybackSession = new PlaybackSession() |     const newPlaybackSession = new PlaybackSession() | ||||||
|     newPlaybackSession.setData(libraryItem, user, mediaPlayer, episodeId) |     newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId) | ||||||
| 
 | 
 | ||||||
|     var audioTracks = [] |     var audioTracks = [] | ||||||
|     if (shouldDirectPlay) { |     if (shouldDirectPlay) { | ||||||
| @ -122,7 +140,6 @@ class PlaybackSessionManager { | |||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     newPlaybackSession.currentTime = userStartTime |  | ||||||
|     newPlaybackSession.audioTracks = audioTracks |     newPlaybackSession.audioTracks = audioTracks | ||||||
| 
 | 
 | ||||||
|     // Will save on the first sync
 |     // 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 { PlayMethod } = require('../utils/constants') | ||||||
| const BookMetadata = require('./metadata/BookMetadata') | const BookMetadata = require('./metadata/BookMetadata') | ||||||
| const PodcastMetadata = require('./metadata/PodcastMetadata') | const PodcastMetadata = require('./metadata/PodcastMetadata') | ||||||
|  | const DeviceInfo = require('./DeviceInfo') | ||||||
| 
 | 
 | ||||||
| class PlaybackSession { | class PlaybackSession { | ||||||
|   constructor(session) { |   constructor(session) { | ||||||
| @ -21,18 +22,21 @@ class PlaybackSession { | |||||||
| 
 | 
 | ||||||
|     this.playMethod = null |     this.playMethod = null | ||||||
|     this.mediaPlayer = null |     this.mediaPlayer = null | ||||||
|  |     this.deviceInfo = null | ||||||
| 
 | 
 | ||||||
|     this.date = null |     this.date = null | ||||||
|     this.dayOfWeek = null |     this.dayOfWeek = null | ||||||
| 
 | 
 | ||||||
|     this.timeListening = 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.startedAt = null | ||||||
|     this.updatedAt = null |     this.updatedAt = null | ||||||
| 
 | 
 | ||||||
|     // Not saved in DB
 |     // Not saved in DB
 | ||||||
|     this.lastSave = 0 |     this.lastSave = 0 | ||||||
|     this.audioTracks = [] |     this.audioTracks = [] | ||||||
|     this.currentTime = 0 |  | ||||||
|     this.stream = null |     this.stream = null | ||||||
| 
 | 
 | ||||||
|     if (session) { |     if (session) { | ||||||
| @ -56,10 +60,13 @@ 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, | ||||||
|       date: this.date, |       date: this.date, | ||||||
|       dayOfWeek: this.dayOfWeek, |       dayOfWeek: this.dayOfWeek, | ||||||
|       timeListening: this.timeListening, |       timeListening: this.timeListening, | ||||||
|       lastUpdate: this.lastUpdate, |       startTime: this.startTime, | ||||||
|  |       currentTime: this.currentTime, | ||||||
|  |       startedAt: this.startedAt, | ||||||
|       updatedAt: this.updatedAt |       updatedAt: this.updatedAt | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -80,13 +87,15 @@ 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, | ||||||
|       date: this.date, |       date: this.date, | ||||||
|       dayOfWeek: this.dayOfWeek, |       dayOfWeek: this.dayOfWeek, | ||||||
|       timeListening: this.timeListening, |       timeListening: this.timeListening, | ||||||
|       lastUpdate: this.lastUpdate, |       startTime: this.startTime, | ||||||
|  |       currentTime: this.currentTime, | ||||||
|  |       startedAt: this.startedAt, | ||||||
|       updatedAt: this.updatedAt, |       updatedAt: this.updatedAt, | ||||||
|       audioTracks: this.audioTracks.map(at => at.toJSON()), |       audioTracks: this.audioTracks.map(at => at.toJSON()), | ||||||
|       currentTime: this.currentTime, |  | ||||||
|       libraryItem: libraryItem.toJSONExpanded() |       libraryItem: libraryItem.toJSONExpanded() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @ -101,6 +110,7 @@ class PlaybackSession { | |||||||
|     this.duration = session.duration |     this.duration = session.duration | ||||||
|     this.playMethod = session.playMethod |     this.playMethod = session.playMethod | ||||||
|     this.mediaPlayer = session.mediaPlayer || null |     this.mediaPlayer = session.mediaPlayer || null | ||||||
|  |     this.deviceInfo = new DeviceInfo(session.deviceInfo) | ||||||
|     this.chapters = session.chapters || [] |     this.chapters = session.chapters || [] | ||||||
| 
 | 
 | ||||||
|     this.mediaMetadata = null |     this.mediaMetadata = null | ||||||
| @ -118,6 +128,9 @@ class PlaybackSession { | |||||||
|     this.dayOfWeek = session.dayOfWeek |     this.dayOfWeek = session.dayOfWeek | ||||||
| 
 | 
 | ||||||
|     this.timeListening = session.timeListening || null |     this.timeListening = session.timeListening || null | ||||||
|  |     this.startTime = session.startTime || 0 | ||||||
|  |     this.currentTime = session.currentTime || 0 | ||||||
|  | 
 | ||||||
|     this.startedAt = session.startedAt |     this.startedAt = session.startedAt | ||||||
|     this.updatedAt = session.updatedAt || null |     this.updatedAt = session.updatedAt || null | ||||||
|   } |   } | ||||||
| @ -127,7 +140,7 @@ class PlaybackSession { | |||||||
|     return Math.max(0, Math.min(this.currentTime / this.duration, 1)) |     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.id = getId('play') | ||||||
|     this.userId = user.id |     this.userId = user.id | ||||||
|     this.libraryItemId = libraryItem.id |     this.libraryItemId = libraryItem.id | ||||||
| @ -146,8 +159,13 @@ class PlaybackSession { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.mediaPlayer = mediaPlayer |     this.mediaPlayer = mediaPlayer | ||||||
|  |     this.deviceInfo = deviceInfo || new DeviceInfo() | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     this.timeListening = 0 |     this.timeListening = 0 | ||||||
|  |     this.startTime = startTime | ||||||
|  |     this.currentTime = startTime | ||||||
|  | 
 | ||||||
|     this.date = date.format(new Date(), 'YYYY-MM-DD') |     this.date = date.format(new Date(), 'YYYY-MM-DD') | ||||||
|     this.dayOfWeek = date.format(new Date(), 'dddd') |     this.dayOfWeek = date.format(new Date(), 'dddd') | ||||||
|     this.startedAt = Date.now() |     this.startedAt = Date.now() | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user