From 8e286a6070d5c41558336fda44f437a9de525359 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 30 Jun 2024 16:36:00 -0500 Subject: [PATCH] Open media item share sessions shown on listening sessions page, create device info for share sessions --- .../modals/ListeningSessionModal.vue | 11 +++-- client/pages/config/sessions.vue | 43 ++++++++++++++++++- client/pages/share/_slug.vue | 2 +- server/Server.js | 2 +- server/controllers/SessionController.js | 37 ++++++++-------- server/controllers/ShareController.js | 26 +++++++---- server/managers/PlaybackSessionManager.js | 18 +++++--- server/routers/PublicRouter.js | 5 ++- 8 files changed, 104 insertions(+), 40 deletions(-) diff --git a/client/components/modals/ListeningSessionModal.vue b/client/components/modals/ListeningSessionModal.vue index 6b4a0d8a..0b18980e 100644 --- a/client/components/modals/ListeningSessionModal.vue +++ b/client/components/modals/ListeningSessionModal.vue @@ -80,8 +80,8 @@
-

{{ $strings.LabelUser }}

-

{{ _session.userId }}

+

{{ $strings.LabelUser }}

+

{{ _session.userId }}

{{ $strings.LabelMediaPlayer }}

{{ playMethodName }}

@@ -99,8 +99,8 @@
- {{ $strings.ButtonDelete }} - Close Open Session + {{ $strings.ButtonDelete }} + Close Open Session
@@ -166,6 +166,9 @@ export default { }, isOpenSession() { return !!this._session.open + }, + isMediaItemShareSession() { + return this._session.mediaPlayer === 'web-share' } }, methods: { diff --git a/client/pages/config/sessions.vue b/client/pages/config/sessions.vue index 5ea97b62..9d3d0f0e 100644 --- a/client/pages/config/sessions.vue +++ b/client/pages/config/sessions.vue @@ -100,7 +100,7 @@

{{ $strings.MessageNoListeningSessions }}

-
+

Open Listening Sessions

@@ -144,6 +144,45 @@
+ +
+ + +

Open Share Listening Sessions

+
+ + + + + + + + + + + + + + + + + + +
{{ $strings.LabelItem }}{{ $strings.LabelLastTime }}
+

{{ session.displayTitle }}

+

{{ session.displayAuthor }}

+
+

{{ $secondsToTimestamp(session.currentTime) }}

+
+
@@ -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) diff --git a/client/pages/share/_slug.vue b/client/pages/share/_slug.vue index 8fd715bd..14d98c3f 100644 --- a/client/pages/share/_slug.vue +++ b/client/pages/share/_slug.vue @@ -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 }) diff --git a/server/Server.js b/server/Server.js index 6428d0fc..69061573 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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() diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 7626bd12..9dd3666d 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -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() \ No newline at end of file +module.exports = new SessionController() diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 38b19479..96f8788c 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -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') } diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 1d292b3d..414f7f71 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -35,14 +35,18 @@ class PlaybackSessionManager { return session?.stream || null } - async getDeviceInfo(req) { + /** + * + * @param {import('express').Request} req + * @param {Object} [clientDeviceInfo] + * @returns {Promise} + */ + 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) diff --git a/server/routers/PublicRouter.js b/server/routers/PublicRouter.js index 329a4066..98ac4955 100644 --- a/server/routers/PublicRouter.js +++ b/server/routers/PublicRouter.js @@ -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()