mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 10:27:01 -04:00 
			
		
		
		
	Add:All listening sessions config page
This commit is contained in:
		
							parent
							
								
									2d5e4ebcf0
								
							
						
					
					
						commit
						5cd343cb01
					
				| @ -38,12 +38,6 @@ export default { | |||||||
|     userIsAdminOrUp() { |     userIsAdminOrUp() { | ||||||
|       return this.$store.getters['user/getIsAdminOrUp'] |       return this.$store.getters['user/getIsAdminOrUp'] | ||||||
|     }, |     }, | ||||||
|     user() { |  | ||||||
|       return this.$store.state.user.user || {} |  | ||||||
|     }, |  | ||||||
|     userId() { |  | ||||||
|       return this.user.id |  | ||||||
|     }, |  | ||||||
|     configRoutes() { |     configRoutes() { | ||||||
|       if (!this.userIsAdminOrUp) { |       if (!this.userIsAdminOrUp) { | ||||||
|         return [ |         return [ | ||||||
| @ -70,6 +64,11 @@ export default { | |||||||
|           title: 'Users', |           title: 'Users', | ||||||
|           path: '/config/users' |           path: '/config/users' | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |           id: 'config-sessions', | ||||||
|  |           title: 'Sessions', | ||||||
|  |           path: '/config/sessions' | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|           id: 'config-backups', |           id: 'config-backups', | ||||||
|           title: 'Backups', |           title: 'Backups', | ||||||
| @ -93,11 +92,6 @@ export default { | |||||||
|           title: 'Your Stats', |           title: 'Your Stats', | ||||||
|           path: '/config/stats' |           path: '/config/stats' | ||||||
|         }) |         }) | ||||||
|         configRoutes.push({ |  | ||||||
|           id: 'config-users-id-sessions', |  | ||||||
|           title: 'Your Sessions', |  | ||||||
|           path: `/config/users/${this.userId}/sessions` |  | ||||||
|         }) |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       return configRoutes |       return configRoutes | ||||||
|  | |||||||
| @ -42,7 +42,8 @@ export default { | |||||||
|     editable: { |     editable: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: true |       default: true | ||||||
|     } |     }, | ||||||
|  |     showAllWhenEmpty: Boolean | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
| @ -72,6 +73,7 @@ export default { | |||||||
|     itemsToShow() { |     itemsToShow() { | ||||||
|       if (!this.editable) return this.items |       if (!this.editable) return this.items | ||||||
|       if (!this.textInput || this.textInput === this.input) { |       if (!this.textInput || this.textInput === this.input) { | ||||||
|  |         if (this.showAllWhenEmpty) return this.items | ||||||
|         return [] |         return [] | ||||||
|       } |       } | ||||||
|       return this.items.filter((i) => { |       return this.items.filter((i) => { | ||||||
|  | |||||||
							
								
								
									
										194
									
								
								client/pages/config/sessions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								client/pages/config/sessions.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | |||||||
|  | <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"> | ||||||
|  |       <div class="py-2"> | ||||||
|  |         <div class="flex items-center mb-1"> | ||||||
|  |           <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1> | ||||||
|  |           <div class="flex-grow" /> | ||||||
|  | 
 | ||||||
|  |           <ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="listeningSessions.length"> | ||||||
|  |           <table class="userSessionsTable"> | ||||||
|  |             <tr class="bg-primary bg-opacity-40"> | ||||||
|  |               <th class="flex-grow text-left">Item</th> | ||||||
|  |               <th class="w-20 text-left">User</th> | ||||||
|  |               <th class="w-32 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">Listened</th> | ||||||
|  |               <th class="w-20">Last Time</th> | ||||||
|  |               <th class="w-40 hidden sm:table-cell">Last Update</th> | ||||||
|  |             </tr> | ||||||
|  |             <tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)"> | ||||||
|  |               <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 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"> | ||||||
|  |                 <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="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')"> | ||||||
|  |                   <p class="text-xs">{{ $dateDistanceFromNow(session.updatedAt) }}</p> | ||||||
|  |                 </ui-tooltip> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           </table> | ||||||
|  |           <div class="flex items-center justify-end py-1"> | ||||||
|  |             <ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> | ||||||
|  |             <p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p> | ||||||
|  |             <ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <p v-else class="text-white text-opacity-50">No sessions yet...</p> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   async asyncData({ params, redirect, app }) { | ||||||
|  |     var users = await app.$axios | ||||||
|  |       .$get('/api/users') | ||||||
|  |       .then((users) => { | ||||||
|  |         return users.sort((a, b) => { | ||||||
|  |           return a.createdAt - b.createdAt | ||||||
|  |         }) | ||||||
|  |       }) | ||||||
|  |       .catch((error) => { | ||||||
|  |         console.error('Failed', error) | ||||||
|  |         return [] | ||||||
|  |       }) | ||||||
|  |     return { | ||||||
|  |       users | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       showSessionModal: false, | ||||||
|  |       selectedSession: null, | ||||||
|  |       listeningSessions: [], | ||||||
|  |       numPages: 0, | ||||||
|  |       total: 0, | ||||||
|  |       currentPage: 0, | ||||||
|  |       userFilter: null, | ||||||
|  |       selectedUser: '' | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     username() { | ||||||
|  |       return this.user.username | ||||||
|  |     }, | ||||||
|  |     userOnline() { | ||||||
|  |       return this.$store.getters['users/getIsUserOnline'](this.user.id) | ||||||
|  |     }, | ||||||
|  |     userItems() { | ||||||
|  |       var userItems = [{ value: '', text: 'All Users' }] | ||||||
|  |       return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username }))) | ||||||
|  |     }, | ||||||
|  |     filteredUserUsername() { | ||||||
|  |       if (!this.userFilter) return null | ||||||
|  |       var user = this.users.find((u) => u.id === this.userFilter) | ||||||
|  |       return user ? user.username : null | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     updateUserFilter() { | ||||||
|  |       this.loadSessions(0) | ||||||
|  |     }, | ||||||
|  |     prevPage() { | ||||||
|  |       this.loadSessions(this.currentPage - 1) | ||||||
|  |     }, | ||||||
|  |     nextPage() { | ||||||
|  |       this.loadSessions(this.currentPage + 1) | ||||||
|  |     }, | ||||||
|  |     showSession(session) { | ||||||
|  |       this.selectedSession = session | ||||||
|  |       this.showSessionModal = true | ||||||
|  |     }, | ||||||
|  |     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 loadSessions(page) { | ||||||
|  |       var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : '' | ||||||
|  |       const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=10${userFilterQuery}`).catch((err) => { | ||||||
|  |         console.error('Failed to load listening sesions', err) | ||||||
|  |         return null | ||||||
|  |       }) | ||||||
|  |       if (!data) { | ||||||
|  |         this.$toast.error('Failed to load listening sessions') | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.numPages = data.numPages | ||||||
|  |       this.total = data.total | ||||||
|  |       this.currentPage = data.page | ||||||
|  |       this.listeningSessions = data.sessions | ||||||
|  |       this.userFilter = data.userFilter | ||||||
|  |     }, | ||||||
|  |     init() { | ||||||
|  |       this.loadSessions(0) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.init() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .userSessionsTable { | ||||||
|  |   border-collapse: collapse; | ||||||
|  |   width: 100%; | ||||||
|  |   border: 1px solid #474747; | ||||||
|  | } | ||||||
|  | .userSessionsTable tr:first-child { | ||||||
|  |   background-color: #272727; | ||||||
|  | } | ||||||
|  | .userSessionsTable tr:not(:first-child) { | ||||||
|  |   background-color: #373838; | ||||||
|  | } | ||||||
|  | .userSessionsTable tr:not(:first-child):nth-child(odd) { | ||||||
|  |   background-color: #2f2f2f; | ||||||
|  | } | ||||||
|  | .userSessionsTable tr:hover:not(:first-child) { | ||||||
|  |   background-color: #474747; | ||||||
|  | } | ||||||
|  | .userSessionsTable td { | ||||||
|  |   padding: 4px 8px; | ||||||
|  | } | ||||||
|  | .userSessionsTable th { | ||||||
|  |   padding: 4px 8px; | ||||||
|  |   font-size: 0.75rem; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -17,7 +17,7 @@ | |||||||
|       <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 Sessions ({{ listeningSessions.length }})</h1> |         <h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1> | ||||||
|         <div v-if="listeningSessions.length"> |         <div v-if="listeningSessions.length"> | ||||||
|           <table class="userSessionsTable"> |           <table class="userSessionsTable"> | ||||||
|             <tr class="bg-primary bg-opacity-40"> |             <tr class="bg-primary bg-opacity-40"> | ||||||
|  | |||||||
| @ -428,6 +428,15 @@ class Db { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   getAllSessions() { | ||||||
|  |     return this.sessionsDb.select(() => true).then((results) => { | ||||||
|  |       return results.data || [] | ||||||
|  |     }).catch((error) => { | ||||||
|  |       Logger.error('[Db] Failed to select sessions', error) | ||||||
|  |       return [] | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   selectUserSessions(userId) { |   selectUserSessions(userId) { | ||||||
|     return this.sessionsDb.select((session) => session.userId === userId).then((results) => { |     return this.sessionsDb.select((session) => session.userId === userId).then((results) => { | ||||||
|       return results.data || [] |       return results.data || [] | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
|  | const { toNumber } = require('../utils/index') | ||||||
| 
 | 
 | ||||||
| class SessionController { | class SessionController { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| @ -7,6 +8,40 @@ class SessionController { | |||||||
|     return res.json(req.session) |     return res.json(req.session) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async getAllWithUserData(req, res) { | ||||||
|  |     if (!req.user.isAdminOrUp) { | ||||||
|  |       Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`) | ||||||
|  |       return res.sendStatus(404) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var listeningSessions = [] | ||||||
|  |     if (req.query.user) { | ||||||
|  |       listeningSessions = await this.getUserListeningSessionsHelper(req.query.user) | ||||||
|  |     } else { | ||||||
|  |       listeningSessions = await this.getAllSessionsWithUserData() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 | ||||||
|  |     const page = toNumber(req.query.page, 0) | ||||||
|  | 
 | ||||||
|  |     const start = page * itemsPerPage | ||||||
|  |     const sessions = listeningSessions.slice(start, start + itemsPerPage) | ||||||
|  | 
 | ||||||
|  |     const payload = { | ||||||
|  |       total: listeningSessions.length, | ||||||
|  |       numPages: Math.ceil(listeningSessions.length / itemsPerPage), | ||||||
|  |       page, | ||||||
|  |       itemsPerPage, | ||||||
|  |       sessions | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (req.query.user) { | ||||||
|  |       payload.userFilter = req.query.user | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     res.json(payload) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getSession(req, res) { |   getSession(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) | ||||||
|  | |||||||
| @ -174,6 +174,7 @@ class ApiRouter { | |||||||
|     //
 |     //
 | ||||||
|     // Playback Session Routes
 |     // Playback Session Routes
 | ||||||
|     //
 |     //
 | ||||||
|  |     this.router.get('/sessions', SessionController.getAllWithUserData.bind(this)) | ||||||
|     this.router.get('/session/:id', SessionController.middleware.bind(this), SessionController.getSession.bind(this)) |     this.router.get('/session/:id', SessionController.middleware.bind(this), SessionController.getSession.bind(this)) | ||||||
|     this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this)) |     this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this)) | ||||||
|     this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this)) |     this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this)) | ||||||
| @ -310,6 +311,19 @@ class ApiRouter { | |||||||
|     return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) |     return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async getAllSessionsWithUserData() { | ||||||
|  |     var sessions = await this.db.getAllSessions() | ||||||
|  |     sessions.sort((a, b) => b.updatedAt - a.updatedAt) | ||||||
|  |     return sessions.map(se => { | ||||||
|  |       var user = this.db.users.find(u => u.id === se.userId) | ||||||
|  |       var _se = { | ||||||
|  |         ...se, | ||||||
|  |         user: user ? { id: user.id, username: user.username } : null | ||||||
|  |       } | ||||||
|  |       return _se | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async getUserListeningStatsHelpers(userId) { |   async getUserListeningStatsHelpers(userId) { | ||||||
|     const today = date.format(new Date(), 'YYYY-MM-DD') |     const today = date.format(new Date(), 'YYYY-MM-DD') | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user