Update share endpoint to return playback session, add get share file endpoint

This commit is contained in:
advplyr 2024-06-26 17:03:12 -05:00
parent 042035051d
commit 8cadaa57f6
6 changed files with 161 additions and 12 deletions

View File

@ -1,12 +1,20 @@
<template> <template>
<div id="page-wrapper" class="w-full h-screen overflow-y-auto"> <div id="page-wrapper" class="w-full h-screen overflow-y-auto">
<div class="w-full h-full flex items-center justify-center"> <div class="w-full h-full flex items-center justify-center">
<p class="text-xl">{{ mediaItemShare.mediaItem.title }}</p> <div>
<p class="text-3xl font-semibold text-center mb-6">{{ mediaItemShare.playbackSession?.displayTitle || 'N/A' }}</p>
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-4 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-auto" :class="!hasLoaded ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<span class="material-icons text-5xl">{{ !hasLoaded ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import LocalAudioPlayer from '../../players/LocalAudioPlayer'
export default { export default {
layout: 'blank', layout: 'blank',
async asyncData({ params, error, app }) { async asyncData({ params, error, app }) {
@ -23,12 +31,43 @@ export default {
} }
}, },
data() { data() {
return {} return {
localAudioPlayer: new LocalAudioPlayer(),
playerState: null,
hasLoaded: false
}
},
computed: {
audioTracks() {
return (this.mediaItemShare.playbackSession?.audioTracks || []).map((track) => {
if (process.env.NODE_ENV === 'development') {
track.contentUrl = `${process.env.serverUrl}${track.contentUrl}`
}
track.relativeContentUrl = track.contentUrl
return track
})
},
paused() {
return this.playerState !== 'PLAYING'
}
},
methods: {
playPause() {
if (!this.localAudioPlayer || this.mediaLoading) return
this.localAudioPlayer.playPause()
},
playerStateChange(state) {
console.log('Player state change', state)
this.playerState = state
if (state === 'LOADED') {
this.hasLoaded = true
}
}
}, },
computed: {},
methods: {},
mounted() { mounted() {
console.log('Loaded media item share', this.mediaItemShare) console.log('Loaded media item share', this.mediaItemShare)
this.localAudioPlayer.set(null, this.audioTracks, false, 0, false)
this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this))
} }
} }
</script> </script>

View File

@ -137,6 +137,11 @@ class Database {
return this.models.customMetadataProvider return this.models.customMetadataProvider
} }
/** @type {typeof import('./models/MediaItemShare')} */
get mediaItemShareModel() {
return this.models.mediaItemShare
}
/** /**
* Check if db file exists * Check if db file exists
* @returns {boolean} * @returns {boolean}

View File

@ -1,7 +1,12 @@
const Path = require('path')
const { Op } = require('sequelize') const { Op } = require('sequelize')
const Logger = require('../Logger') const Logger = require('../Logger')
const Database = require('../Database') const Database = require('../Database')
const { PlayMethod } = require('../utils/constants')
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
const PlaybackSession = require('../objects/PlaybackSession')
const ShareManager = require('../managers/ShareManager') const ShareManager = require('../managers/ShareManager')
class ShareController { class ShareController {
@ -9,7 +14,7 @@ class ShareController {
/** /**
* Public route * Public route
* GET: /api/share/mediaitem/:slug * GET: /api/share/:slug
* Get media item share by slug * Get media item share by slug
* *
* @param {import('express').Request} req * @param {import('express').Request} req
@ -28,13 +33,35 @@ class ShareController {
} }
try { try {
const mediaItemModel = mediaItemShare.mediaItemType === 'book' ? Database.bookModel : Database.podcastEpisodeModel const oldLibraryItem = await Database.mediaItemShareModel.getMediaItemsOldLibraryItem(mediaItemShare.mediaItemId, mediaItemShare.mediaItemType)
mediaItemShare.mediaItem = await mediaItemModel.findByPk(mediaItemShare.mediaItemId)
if (!mediaItemShare.mediaItem) { if (!oldLibraryItem) {
return res.status(404).send('Media item not found') return res.status(404).send('Media item not found')
} }
let startOffset = 0
const publicTracks = oldLibraryItem.media.includedAudioFiles.map((audioFile) => {
const audioTrack = {
index: audioFile.index,
startOffset,
duration: audioFile.duration,
title: audioFile.metadata.filename || '',
contentUrl: `${global.RouterBasePath}/public/share/${slug}/file/${audioFile.ino}`,
mimeType: audioFile.mimeType,
codec: audioFile.codec || null,
metadata: audioFile.metadata.clone()
}
startOffset += audioTrack.duration
return audioTrack
})
const newPlaybackSession = new PlaybackSession()
newPlaybackSession.setData(oldLibraryItem, null, 'web-public', null, 0)
newPlaybackSession.audioTracks = publicTracks
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
mediaItemShare.playbackSession = newPlaybackSession.toJSONForClient()
res.json(mediaItemShare) res.json(mediaItemShare)
} catch (error) { } catch (error) {
Logger.error(`[ShareController] Failed`, error) Logger.error(`[ShareController] Failed`, error)
@ -42,6 +69,48 @@ class ShareController {
} }
} }
/**
* Public route
* GET: /api/share/:slug/file/:fileid
* Get media item share file
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async getMediaItemShareFile(req, res) {
const { slug, fileid } = req.params
const mediaItemShare = ShareManager.findBySlug(slug)
if (!mediaItemShare) {
return res.status(404)
}
/** @type {import('../models/LibraryItem')} */
const libraryItem = await Database.libraryItemModel.findOne({
where: {
mediaId: mediaItemShare.mediaItemId
}
})
const libraryFile = libraryItem?.libraryFiles.find((lf) => lf.ino === fileid)
if (!libraryFile) {
return res.status(404).send('File not found')
}
if (global.XAccel) {
const encodedURI = encodeUriPath(global.XAccel + libraryFile.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(libraryFile.metadata.path))
if (audioMimeType) {
res.setHeader('Content-Type', audioMimeType)
}
res.sendFile(libraryFile.metadata.path)
}
/** /**
* POST: /api/share/mediaitem * POST: /api/share/mediaitem
* Create a new media item share * Create a new media item share
@ -69,7 +138,7 @@ class ShareController {
try { try {
// Check if the media item share already exists by slug or mediaItemId // Check if the media item share already exists by slug or mediaItemId
const existingMediaItemShare = await Database.models.mediaItemShare.findOne({ const existingMediaItemShare = await Database.mediaItemShareModel.findOne({
where: { where: {
[Op.or]: [{ slug }, { mediaItemId }] [Op.or]: [{ slug }, { mediaItemId }]
} }
@ -89,7 +158,7 @@ class ShareController {
return res.status(404).send('Media item not found') return res.status(404).send('Media item not found')
} }
const mediaItemShare = await Database.models.mediaItemShare.create({ const mediaItemShare = await Database.mediaItemShareModel.create({
slug, slug,
expiresAt: expiresAt || null, expiresAt: expiresAt || null,
mediaItemId, mediaItemId,
@ -120,7 +189,7 @@ class ShareController {
} }
try { try {
const mediaItemShare = await Database.models.mediaItemShare.findByPk(req.params.id) const mediaItemShare = await Database.mediaItemShareModel.findByPk(req.params.id)
if (!mediaItemShare) { if (!mediaItemShare) {
return res.status(404).send('Media item share not found') return res.status(404).send('Media item share not found')
} }

View File

@ -44,6 +44,41 @@ class MediaItemShare extends Model {
} }
} }
/**
*
* @param {string} mediaItemId
* @param {string} mediaItemType
* @returns {Promise<import('../objects/LibraryItem')>}
*/
static async getMediaItemsOldLibraryItem(mediaItemId, mediaItemType) {
if (mediaItemType === 'book') {
const book = await this.sequelize.models.book.findByPk(mediaItemId, {
include: [
{
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
},
{
model: this.sequelize.models.libraryItem
}
]
})
const libraryItem = book.libraryItem
libraryItem.media = book
delete book.libraryItem
return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
}
return null
}
/** /**
* *
* @param {import('sequelize').FindOptions} options * @param {import('sequelize').FindOptions} options

View File

@ -109,7 +109,7 @@ class PlaybackSession {
currentTime: this.currentTime, currentTime: this.currentTime,
startedAt: this.startedAt, startedAt: this.startedAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map((at) => at.toJSON()), audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }),
videoTrack: this.videoTrack?.toJSON() || null, videoTrack: this.videoTrack?.toJSON() || null,
libraryItem: libraryItem?.toJSONExpanded() || null libraryItem: libraryItem?.toJSONExpanded() || null
} }

View File

@ -10,6 +10,7 @@ class PublicRouter {
init() { init() {
this.router.get('/share/:slug', ShareController.getMediaItemShareBySlug.bind(this)) this.router.get('/share/:slug', ShareController.getMediaItemShareBySlug.bind(this))
this.router.get('/share/:slug/file/:fileid', ShareController.getMediaItemShareFile.bind(this))
} }
} }
module.exports = PublicRouter module.exports = PublicRouter