mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Update share endpoint to return playback session, add get share file endpoint
This commit is contained in:
parent
042035051d
commit
8cadaa57f6
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user