Compare commits

...

6 Commits

Author SHA1 Message Date
advplyr 1f7b181b7b Update HLS stream endpoints to not include user token 2025-05-06 17:28:19 -05:00
advplyr 1afb8840db Merge pull request #4263 from advplyr/new_session_track_endpoint
Add new api endpoint for direct playing audio files using session id
2025-05-05 17:25:40 -05:00
advplyr d9531166b6 Fix for HLS transcode urls 2025-05-05 17:07:51 -05:00
advplyr 336de49d8d Add new api endpoint for direct playing audio files using session id #4259 2025-05-05 17:00:43 -05:00
advplyr 45987ffd63 Fix library stats returning null instead of 0 #4251 2025-05-03 17:25:01 -05:00
advplyr 1a1ef9c378 Merge pull request #4253 from advplyr/audiobook_tools_enhancements
Audiobook tools enhancements
2025-05-02 17:43:39 -05:00
8 changed files with 77 additions and 17 deletions
+10 -9
View File
@@ -1,5 +1,5 @@
export default class AudioTrack { export default class AudioTrack {
constructor(track, userToken, routerBasePath) { constructor(track, sessionId, routerBasePath) {
this.index = track.index || 0 this.index = track.index || 0
this.startOffset = track.startOffset || 0 // Total time of all previous tracks this.startOffset = track.startOffset || 0 // Total time of all previous tracks
this.duration = track.duration || 0 this.duration = track.duration || 0
@@ -8,28 +8,29 @@ export default class AudioTrack {
this.mimeType = track.mimeType this.mimeType = track.mimeType
this.metadata = track.metadata || {} this.metadata = track.metadata || {}
this.userToken = userToken this.sessionId = sessionId
this.routerBasePath = routerBasePath || '' this.routerBasePath = routerBasePath || ''
if (this.contentUrl?.startsWith('/hls')) {
this.sessionTrackUrl = this.contentUrl
} else {
this.sessionTrackUrl = `/public/session/${sessionId}/track/${this.index}`
}
} }
/** /**
* Used for CastPlayer * Used for CastPlayer
*/ */
get fullContentUrl() { get fullContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}` return `${process.env.serverUrl}${this.sessionTrackUrl}`
} }
return `${window.location.origin}${this.routerBasePath}${this.contentUrl}?token=${this.userToken}` return `${window.location.origin}${this.routerBasePath}${this.sessionTrackUrl}`
} }
/** /**
* Used for LocalPlayer * Used for LocalPlayer
*/ */
get relativeContentUrl() { get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl return `${this.routerBasePath}${this.sessionTrackUrl}`
return `${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
} }
} }
+1 -4
View File
@@ -37,9 +37,6 @@ export default class PlayerHandler {
get isPlayingLocalItem() { get isPlayingLocalItem() {
return this.libraryItem && this.player instanceof LocalAudioPlayer return this.libraryItem && this.player instanceof LocalAudioPlayer
} }
get userToken() {
return this.ctx.$store.getters['user/getToken']
}
get playerPlaying() { get playerPlaying() {
return this.playerState === 'PLAYING' return this.playerState === 'PLAYING'
} }
@@ -226,7 +223,7 @@ export default class PlayerHandler {
console.log('[PlayerHandler] Preparing Session', session) console.log('[PlayerHandler] Preparing Session', session)
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken, this.ctx.$config.routerBasePath)) var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, session.id, this.ctx.$config.routerBasePath))
this.ctx.playerLoading = true this.ctx.playerLoading = true
this.isHlsTranscode = true this.isHlsTranscode = true
+1 -1
View File
@@ -313,7 +313,7 @@ class Server {
router.use(express.json({ limit: '5mb' })) router.use(express.json({ limit: '5mb' }))
router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router) router.use('/api', this.auth.ifAuthNeeded(this.authMiddleware.bind(this)), this.apiRouter.router)
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router) router.use('/hls', this.hlsRouter.router)
router.use('/public', this.publicRouter.router) router.use('/public', this.publicRouter.router)
// Static path to generated nuxt // Static path to generated nuxt
+47
View File
@@ -1,7 +1,9 @@
const Path = require('path')
const { Request, Response, NextFunction } = require('express') const { Request, Response, NextFunction } = require('express')
const Logger = require('../Logger') const Logger = require('../Logger')
const Database = require('../Database') const Database = require('../Database')
const { toNumber, isUUID } = require('../utils/index') const { toNumber, isUUID } = require('../utils/index')
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
const ShareManager = require('../managers/ShareManager') const ShareManager = require('../managers/ShareManager')
@@ -266,6 +268,51 @@ class SessionController {
this.playbackSessionManager.syncLocalSessionsRequest(req, res) this.playbackSessionManager.syncLocalSessionsRequest(req, res)
} }
/**
* GET: /public/session/:id/track/:index
* While a session is open, this endpoint can be used to stream the audio track
*
* @this {import('../routers/PublicRouter')}
*
* @param {Request} req
* @param {Response} res
*/
async getTrack(req, res) {
const audioTrackIndex = toNumber(req.params.index, null)
if (audioTrackIndex === null) {
Logger.error(`[SessionController] Invalid audio track index "${req.params.index}"`)
return res.sendStatus(400)
}
const playbackSession = this.playbackSessionManager.getSession(req.params.id)
if (!playbackSession) {
Logger.error(`[SessionController] Unable to find playback session with id=${req.params.id}`)
return res.sendStatus(404)
}
const audioTrack = playbackSession.audioTracks.find((t) => t.index === audioTrackIndex)
if (!audioTrack) {
Logger.error(`[SessionController] Unable to find audio track with index=${audioTrackIndex}`)
return res.sendStatus(404)
}
const user = await Database.userModel.getUserById(playbackSession.userId)
Logger.debug(`[SessionController] Serving audio track ${audioTrack.index} for session "${req.params.id}" belonging to user "${user.username}"`)
if (global.XAccel) {
const encodedURI = encodeUriPath(global.XAccel + audioTrack.metadata.path)
Logger.debug(`Use X-Accel to serve static file ${encodedURI}`)
return res.status(204).header({ 'X-Accel-Redirect': encodedURI }).send()
}
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(audioTrack.metadata.path))
if (audioMimeType) {
res.setHeader('Content-Type', audioMimeType)
}
res.sendFile(audioTrack.metadata.path)
}
/** /**
* *
* @param {RequestWithUser} req * @param {RequestWithUser} req
@@ -26,6 +26,12 @@ class PlaybackSessionManager {
this.sessions = [] this.sessions = []
} }
/**
* Get open session by id
*
* @param {string} sessionId
* @returns {PlaybackSession}
*/
getSession(sessionId) { getSession(sessionId) {
return this.sessions.find((s) => s.id === sessionId) return this.sessions.find((s) => s.id === sessionId)
} }
+2
View File
@@ -1,5 +1,6 @@
const express = require('express') const express = require('express')
const ShareController = require('../controllers/ShareController') const ShareController = require('../controllers/ShareController')
const SessionController = require('../controllers/SessionController')
class PublicRouter { class PublicRouter {
constructor(playbackSessionManager) { constructor(playbackSessionManager) {
@@ -17,6 +18,7 @@ class PublicRouter {
this.router.get('/share/:slug/cover', ShareController.getMediaItemShareCoverImage.bind(this)) this.router.get('/share/:slug/cover', ShareController.getMediaItemShareCoverImage.bind(this))
this.router.get('/share/:slug/download', ShareController.downloadMediaItemShare.bind(this)) this.router.get('/share/:slug/download', ShareController.downloadMediaItemShare.bind(this))
this.router.patch('/share/:slug/progress', ShareController.updateMediaItemShareProgress.bind(this)) this.router.patch('/share/:slug/progress', ShareController.updateMediaItemShareProgress.bind(this))
this.router.get('/session/:id/track/:index', SessionController.getTrack.bind(this))
} }
} }
module.exports = PublicRouter module.exports = PublicRouter
@@ -1247,7 +1247,12 @@ module.exports = {
libraryId libraryId
} }
}) })
return statResults[0] return {
totalSize: statResults?.[0]?.totalSize || 0,
totalDuration: statResults?.[0]?.totalDuration || 0,
numAudioFiles: statResults?.[0]?.numAudioFiles || 0,
totalItems: statResults?.[0]?.totalItems || 0
}
}, },
/** /**
@@ -533,8 +533,10 @@ module.exports = {
} }
}) })
return { return {
...statResults[0], totalDuration: statResults?.[0]?.totalDuration || 0,
totalSize: sizeResults[0].totalSize || 0 numAudioFiles: statResults?.[0]?.numAudioFiles || 0,
totalItems: statResults?.[0]?.totalItems || 0,
totalSize: sizeResults?.[0]?.totalSize || 0
} }
}, },