mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Open media item share sessions shown on listening sessions page, create device info for share sessions
This commit is contained in:
		
							parent
							
								
									d7ace4d1dc
								
							
						
					
					
						commit
						8e286a6070
					
				| @ -80,8 +80,8 @@ | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="w-full md:w-1/3"> | ||||
|           <p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p> | ||||
|           <p class="mb-1 text-xs">{{ _session.userId }}</p> | ||||
|           <p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p> | ||||
|           <p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ _session.userId }}</p> | ||||
| 
 | ||||
|           <p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p> | ||||
|           <p class="mb-1">{{ playMethodName }}</p> | ||||
| @ -99,8 +99,8 @@ | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="flex items-center"> | ||||
|         <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> | ||||
|         <ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> | ||||
|         <ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn> | ||||
|       </div> | ||||
|     </div> | ||||
|   </modals-modal> | ||||
| @ -166,6 +166,9 @@ export default { | ||||
|     }, | ||||
|     isOpenSession() { | ||||
|       return !!this._session.open | ||||
|     }, | ||||
|     isMediaItemShareSession() { | ||||
|       return this._session.mediaPlayer === 'web-share' | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|  | ||||
| @ -100,7 +100,7 @@ | ||||
|       </div> | ||||
|       <p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p> | ||||
| 
 | ||||
|       <div class="w-full my-8 h-px bg-white/10" /> | ||||
|       <div v-if="openListeningSessions.length" class="w-full my-8 h-px bg-white/10" /> | ||||
| 
 | ||||
|       <!-- open listening sessions table --> | ||||
|       <p v-if="openListeningSessions.length" class="text-lg my-4">Open Listening Sessions</p> | ||||
| @ -144,6 +144,45 @@ | ||||
|           </tr> | ||||
|         </table> | ||||
|       </div> | ||||
| 
 | ||||
|       <div v-if="openShareListeningSessions.length" class="w-full my-8 h-px bg-white/10" /> | ||||
| 
 | ||||
|       <!-- open share listening sessions table --> | ||||
|       <p v-if="openShareListeningSessions.length" class="text-lg my-4">Open Share Listening Sessions</p> | ||||
|       <div v-if="openShareListeningSessions.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-16 min-w-16">{{ $strings.LabelLastTime }}</th> | ||||
|             <th class="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th> | ||||
|           </tr> | ||||
| 
 | ||||
|           <tr v-for="session in openShareListeningSessions" :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"></td> | ||||
|             <td class="hidden md:table-cell"> | ||||
|               <p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p> | ||||
|             </td> | ||||
|             <td class="hidden sm:table-cell max-w-32 min-w-32"> | ||||
|               <p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" /> | ||||
|             </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> | ||||
| 
 | ||||
|     <modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" @closedSession="closedSession" /> | ||||
| @ -180,6 +219,7 @@ export default { | ||||
|       selectedSession: null, | ||||
|       listeningSessions: [], | ||||
|       openListeningSessions: [], | ||||
|       openShareListeningSessions: [], | ||||
|       numPages: 0, | ||||
|       total: 0, | ||||
|       currentPage: 0, | ||||
| @ -455,6 +495,7 @@ export default { | ||||
|         s.open = true | ||||
|         return s | ||||
|       }) | ||||
|       this.openShareListeningSessions = data.shareSessions || [] | ||||
|     }, | ||||
|     init() { | ||||
|       this.loadSessions(0) | ||||
|  | ||||
| @ -26,7 +26,7 @@ export default { | ||||
|     if (query.t && !isNaN(query.t)) { | ||||
|       endpoint += `?t=${query.t}` | ||||
|     } | ||||
|     const mediaItemShare = await app.$axios.$get(endpoint).catch((error) => { | ||||
|     const mediaItemShare = await app.$axios.$get(endpoint, { timeout: 10000 }).catch((error) => { | ||||
|       console.error('Failed', error) | ||||
|       return null | ||||
|     }) | ||||
|  | ||||
| @ -81,7 +81,7 @@ class Server { | ||||
|     // Routers
 | ||||
|     this.apiRouter = new ApiRouter(this) | ||||
|     this.hlsRouter = new HlsRouter(this.auth, this.playbackSessionManager) | ||||
|     this.publicRouter = new PublicRouter() | ||||
|     this.publicRouter = new PublicRouter(this.playbackSessionManager) | ||||
| 
 | ||||
|     Logger.logManager = new LogManager() | ||||
| 
 | ||||
|  | ||||
| @ -2,8 +2,10 @@ const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| const { toNumber, isUUID } = require('../utils/index') | ||||
| 
 | ||||
| const ShareManager = require('../managers/ShareManager') | ||||
| 
 | ||||
| class SessionController { | ||||
|   constructor() { } | ||||
|   constructor() {} | ||||
| 
 | ||||
|   async findOne(req, res) { | ||||
|     return res.json(req.playbackSession) | ||||
| @ -68,15 +70,13 @@ class SessionController { | ||||
|     const { rows, count } = await Database.playbackSessionModel.findAndCountAll({ | ||||
|       where, | ||||
|       include, | ||||
|       order: [ | ||||
|         [orderKey, orderDesc] | ||||
|       ], | ||||
|       order: [[orderKey, orderDesc]], | ||||
|       limit: itemsPerPage, | ||||
|       offset: itemsPerPage * page | ||||
|     }) | ||||
| 
 | ||||
|     // Map playback sessions to old playback sessions
 | ||||
|     const sessions = rows.map(session => { | ||||
|     const sessions = rows.map((session) => { | ||||
|       const oldPlaybackSession = Database.playbackSessionModel.getOldPlaybackSession(session) | ||||
|       if (session.user) { | ||||
|         return { | ||||
| @ -112,15 +112,18 @@ class SessionController { | ||||
|     } | ||||
| 
 | ||||
|     const minifiedUserObjects = await Database.userModel.getMinifiedUserObjects() | ||||
|     const openSessions = this.playbackSessionManager.sessions.map(se => { | ||||
|     const openSessions = this.playbackSessionManager.sessions.map((se) => { | ||||
|       return { | ||||
|         ...se.toJSON(), | ||||
|         user: minifiedUserObjects.find(u => u.id === se.userId) || null | ||||
|         user: minifiedUserObjects.find((u) => u.id === se.userId) || null | ||||
|       } | ||||
|     }) | ||||
| 
 | ||||
|     const shareSessions = ShareManager.openSharePlaybackSessions.map((se) => se.toJSON()) | ||||
| 
 | ||||
|     res.json({ | ||||
|       sessions: openSessions | ||||
|       sessions: openSessions, | ||||
|       shareSessions | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -170,7 +173,7 @@ class SessionController { | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
|     // Validate session ids
 | ||||
|     if (!req.body.sessions?.length || !Array.isArray(req.body.sessions) || req.body.sessions.some(s => !isUUID(s))) { | ||||
|     if (!req.body.sessions?.length || !Array.isArray(req.body.sessions) || req.body.sessions.some((s) => !isUUID(s))) { | ||||
|       Logger.error(`[SessionController] Invalid request body. "sessions" array is required`, req.body) | ||||
|       return res.status(400).send('Invalid request body. "sessions" array of session id strings is required.') | ||||
|     } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| const uuidv4 = require('uuid').v4 | ||||
| const uuid = require('uuid') | ||||
| const Path = require('path') | ||||
| const { Op } = require('sequelize') | ||||
| const Logger = require('../Logger') | ||||
| @ -18,6 +18,8 @@ class ShareController { | ||||
|    * GET: /api/share/:slug | ||||
|    * Get media item share by slug | ||||
|    * | ||||
|    * @this {import('../routers/PublicRouter')} | ||||
|    * | ||||
|    * @param {import('express').Request} req | ||||
|    * @param {import('express').Response} res | ||||
|    */ | ||||
| @ -28,7 +30,8 @@ class ShareController { | ||||
| 
 | ||||
|     const mediaItemShare = ShareManager.findBySlug(slug) | ||||
|     if (!mediaItemShare) { | ||||
|       return res.status(404) | ||||
|       Logger.warn(`[ShareController] Media item share not found with slug ${slug}`) | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     if (mediaItemShare.expiresAt && mediaItemShare.expiresAt.valueOf() < Date.now()) { | ||||
|       ShareManager.removeMediaItemShare(mediaItemShare.id) | ||||
| @ -43,7 +46,10 @@ class ShareController { | ||||
|         return res.json(mediaItemShare) | ||||
|       } else { | ||||
|         Logger.info(`[ShareController] Share playback session not found with id ${req.cookies.share_session_id}`) | ||||
|         res.clearCookie('share_session_id') | ||||
|         if (!uuid.validate(req.cookies.share_session_id) || uuid.version(req.cookies.share_session_id) !== 4) { | ||||
|           Logger.warn(`[ShareController] Invalid share session id ${req.cookies.share_session_id}`) | ||||
|           res.clearCookie('share_session_id') | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
| @ -75,11 +81,18 @@ class ShareController { | ||||
|         startTime = 0 | ||||
|       } | ||||
| 
 | ||||
|       const shareSessionId = req.cookies.share_session_id || uuid.v4() | ||||
|       const clientDeviceInfo = { | ||||
|         clientName: 'Abs Web Share', | ||||
|         deviceId: shareSessionId | ||||
|       } | ||||
|       const deviceInfo = await this.playbackSessionManager.getDeviceInfo(req, clientDeviceInfo) | ||||
| 
 | ||||
|       const newPlaybackSession = new PlaybackSession() | ||||
|       newPlaybackSession.setData(oldLibraryItem, null, 'web-public', null, startTime) | ||||
|       newPlaybackSession.setData(oldLibraryItem, null, 'web-share', deviceInfo, startTime) | ||||
|       newPlaybackSession.audioTracks = publicTracks | ||||
|       newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY | ||||
|       newPlaybackSession.shareSessionId = uuidv4() // New share session id
 | ||||
|       newPlaybackSession.shareSessionId = shareSessionId | ||||
|       newPlaybackSession.mediaItemShareId = mediaItemShare.id | ||||
|       newPlaybackSession.coverAspectRatio = oldLibraryItem.librarySettings.coverAspectRatio | ||||
| 
 | ||||
| @ -119,7 +132,6 @@ class ShareController { | ||||
| 
 | ||||
|     const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id) | ||||
|     if (!playbackSession || playbackSession.mediaItemShareId !== mediaItemShare.id) { | ||||
|       res.clearCookie('share_session_id') | ||||
|       return res.status(404).send('Share session not found') | ||||
|     } | ||||
| 
 | ||||
| @ -160,7 +172,6 @@ class ShareController { | ||||
| 
 | ||||
|     const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id) | ||||
|     if (!playbackSession || playbackSession.mediaItemShareId !== mediaItemShare.id) { | ||||
|       res.clearCookie('share_session_id') | ||||
|       return res.status(404).send('Share session not found') | ||||
|     } | ||||
| 
 | ||||
| @ -211,7 +222,6 @@ class ShareController { | ||||
| 
 | ||||
|     const playbackSession = ShareManager.findPlaybackSessionBySessionId(req.cookies.share_session_id) | ||||
|     if (!playbackSession || playbackSession.mediaItemShareId !== mediaItemShare.id) { | ||||
|       res.clearCookie('share_session_id') | ||||
|       return res.status(404).send('Share session not found') | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -35,14 +35,18 @@ class PlaybackSessionManager { | ||||
|     return session?.stream || null | ||||
|   } | ||||
| 
 | ||||
|   async getDeviceInfo(req) { | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('express').Request} req | ||||
|    * @param {Object} [clientDeviceInfo] | ||||
|    * @returns {Promise<DeviceInfo>} | ||||
|    */ | ||||
|   async getDeviceInfo(req, clientDeviceInfo = null) { | ||||
|     const ua = uaParserJs(req.headers['user-agent']) | ||||
|     const ip = requestIp.getClientIp(req) | ||||
| 
 | ||||
|     const clientDeviceInfo = req.body?.deviceInfo || null | ||||
| 
 | ||||
|     const deviceInfo = new DeviceInfo() | ||||
|     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user.id) | ||||
|     deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user?.id) | ||||
| 
 | ||||
|     if (clientDeviceInfo?.deviceId) { | ||||
|       const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId) | ||||
| @ -66,7 +70,7 @@ class PlaybackSessionManager { | ||||
|    * @param {string} [episodeId] | ||||
|    */ | ||||
|   async startSessionRequest(req, res, episodeId) { | ||||
|     const deviceInfo = await this.getDeviceInfo(req) | ||||
|     const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) | ||||
|     Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) | ||||
|     const { user, libraryItem, body: options } = req | ||||
|     const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) | ||||
| @ -82,7 +86,7 @@ class PlaybackSessionManager { | ||||
|   } | ||||
| 
 | ||||
|   async syncLocalSessionsRequest(req, res) { | ||||
|     const deviceInfo = await this.getDeviceInfo(req) | ||||
|     const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) | ||||
|     const user = req.user | ||||
|     const sessions = req.body.sessions || [] | ||||
| 
 | ||||
| @ -199,7 +203,7 @@ class PlaybackSessionManager { | ||||
|   } | ||||
| 
 | ||||
|   async syncLocalSessionRequest(req, res) { | ||||
|     const deviceInfo = await this.getDeviceInfo(req) | ||||
|     const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) | ||||
|     const user = req.user | ||||
|     const sessionJson = req.body | ||||
|     const result = await this.syncLocalSession(user, sessionJson, deviceInfo) | ||||
|  | ||||
| @ -2,7 +2,10 @@ const express = require('express') | ||||
| const ShareController = require('../controllers/ShareController') | ||||
| 
 | ||||
| class PublicRouter { | ||||
|   constructor() { | ||||
|   constructor(playbackSessionManager) { | ||||
|     /** @type {import('../managers/PlaybackSessionManager')} */ | ||||
|     this.playbackSessionManager = playbackSessionManager | ||||
| 
 | ||||
|     this.router = express() | ||||
|     this.router.disable('x-powered-by') | ||||
|     this.init() | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user