Open media item share sessions shown on listening sessions page, create device info for share sessions

This commit is contained in:
advplyr 2024-06-30 16:36:00 -05:00
parent d7ace4d1dc
commit 8e286a6070
8 changed files with 104 additions and 40 deletions

View File

@ -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: {

View File

@ -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)

View File

@ -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
})

View File

@ -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()

View File

@ -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)
@ -12,9 +14,9 @@ class SessionController {
/**
* GET: /api/sessions
* @this import('../routers/ApiRouter')
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async getAllWithUserData(req, res) {
if (!req.user.isAdminOrUp) {
@ -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
})
}
@ -157,12 +160,12 @@ class SessionController {
/**
* POST: /api/sessions/batch/delete
* @this import('../routers/ApiRouter')
*
*
* @typedef batchDeleteReqBody
* @property {string[]} sessions
*
* @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req
* @param {import('express').Response} res
*
* @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req
* @param {import('express').Response} res
*/
async batchDelete(req, res) {
if (!req.user.isAdminOrUp) {
@ -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.')
}
@ -239,4 +242,4 @@ class SessionController {
next()
}
}
module.exports = new SessionController()
module.exports = new SessionController()

View File

@ -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')
}

View File

@ -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)

View File

@ -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()