2024-06-30 23:36:00 +02:00
const uuid = require ( 'uuid' )
2024-06-27 00:03:12 +02:00
const Path = require ( 'path' )
2024-06-23 18:01:25 +02:00
const { Op } = require ( 'sequelize' )
2024-06-22 23:42:13 +02:00
const Logger = require ( '../Logger' )
const Database = require ( '../Database' )
2024-06-27 00:03:12 +02:00
const { PlayMethod } = require ( '../utils/constants' )
const { getAudioMimeTypeFromExtname , encodeUriPath } = require ( '../utils/fileUtils' )
const PlaybackSession = require ( '../objects/PlaybackSession' )
2024-06-22 23:42:13 +02:00
const ShareManager = require ( '../managers/ShareManager' )
class ShareController {
constructor ( ) { }
/ * *
* Public route
2024-06-27 00:03:12 +02:00
* GET : / a p i / s h a r e / : s l u g
2024-06-22 23:42:13 +02:00
* Get media item share by slug
*
2024-06-30 23:36:00 +02:00
* @ this { import ( '../routers/PublicRouter' ) }
*
2024-06-22 23:42:13 +02:00
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async getMediaItemShareBySlug ( req , res ) {
const { slug } = req . params
2024-06-30 22:31:27 +02:00
// Optional start time
let startTime = req . query . t && ! isNaN ( req . query . t ) ? Math . max ( 0 , parseInt ( req . query . t ) ) : 0
2024-06-22 23:42:13 +02:00
const mediaItemShare = ShareManager . findBySlug ( slug )
if ( ! mediaItemShare ) {
2024-06-30 23:36:00 +02:00
Logger . warn ( ` [ShareController] Media item share not found with slug ${ slug } ` )
return res . sendStatus ( 404 )
2024-06-22 23:42:13 +02:00
}
if ( mediaItemShare . expiresAt && mediaItemShare . expiresAt . valueOf ( ) < Date . now ( ) ) {
ShareManager . removeMediaItemShare ( mediaItemShare . id )
return res . status ( 404 ) . send ( 'Media item share not found' )
}
2024-06-29 22:05:35 +02:00
if ( req . cookies . share _session _id ) {
const playbackSession = ShareManager . findPlaybackSessionBySessionId ( req . cookies . share _session _id )
2024-07-04 00:08:30 +02:00
2024-06-29 22:05:35 +02:00
if ( playbackSession ) {
2024-07-04 00:08:30 +02:00
const playbackSessionMediaItemShare = ShareManager . findByMediaItemId ( playbackSession . mediaItemId )
if ( ! playbackSessionMediaItemShare ) {
Logger . error ( ` [ShareController] Share playback session ${ req . cookies . share _session _id } media item share not found with id ${ playbackSession . mediaItemId } ` )
return res . sendStatus ( 500 )
}
if ( playbackSessionMediaItemShare . slug === slug ) {
Logger . debug ( ` [ShareController] Found share playback session ${ req . cookies . share _session _id } ` )
mediaItemShare . playbackSession = playbackSession . toJSONForClient ( )
return res . json ( mediaItemShare )
} else {
// TODO: Close old session and use same session id
Logger . info ( ` [ShareController] Share playback session found with id ${ req . cookies . share _session _id } but media item share slug ${ playbackSessionMediaItemShare . slug } does not match requested slug ${ slug } ` )
res . clearCookie ( 'share_session_id' )
}
2024-06-29 22:05:35 +02:00
} else {
Logger . info ( ` [ShareController] Share playback session not found with id ${ req . cookies . share _session _id } ` )
2024-06-30 23:36:00 +02:00
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' )
}
2024-06-29 22:05:35 +02:00
}
}
2024-06-22 23:42:13 +02:00
try {
2024-06-27 00:03:12 +02:00
const oldLibraryItem = await Database . mediaItemShareModel . getMediaItemsOldLibraryItem ( mediaItemShare . mediaItemId , mediaItemShare . mediaItemType )
2024-06-22 23:42:13 +02:00
2024-06-27 00:03:12 +02:00
if ( ! oldLibraryItem ) {
2024-06-22 23:42:13 +02:00
return res . status ( 404 ) . send ( 'Media item not found' )
}
2024-06-23 18:01:25 +02:00
2024-06-27 00:03:12 +02:00
let startOffset = 0
const publicTracks = oldLibraryItem . media . includedAudioFiles . map ( ( audioFile ) => {
const audioTrack = {
index : audioFile . index ,
startOffset ,
duration : audioFile . duration ,
title : audioFile . metadata . filename || '' ,
2024-06-29 22:05:35 +02:00
contentUrl : ` ${ global . RouterBasePath } /public/share/ ${ slug } /track/ ${ audioFile . index } ` ,
2024-06-27 00:03:12 +02:00
mimeType : audioFile . mimeType ,
codec : audioFile . codec || null ,
metadata : audioFile . metadata . clone ( )
}
startOffset += audioTrack . duration
return audioTrack
} )
2024-06-30 22:31:27 +02:00
if ( startTime > startOffset ) {
Logger . warn ( ` [ShareController] Start time ${ startTime } is greater than total duration ${ startOffset } ` )
startTime = 0
}
2024-06-30 23:36:00 +02:00
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 )
2024-06-27 00:03:12 +02:00
const newPlaybackSession = new PlaybackSession ( )
2024-06-30 23:36:00 +02:00
newPlaybackSession . setData ( oldLibraryItem , null , 'web-share' , deviceInfo , startTime )
2024-06-27 00:03:12 +02:00
newPlaybackSession . audioTracks = publicTracks
newPlaybackSession . playMethod = PlayMethod . DIRECTPLAY
2024-06-30 23:36:00 +02:00
newPlaybackSession . shareSessionId = shareSessionId
2024-06-29 22:05:35 +02:00
newPlaybackSession . mediaItemShareId = mediaItemShare . id
newPlaybackSession . coverAspectRatio = oldLibraryItem . librarySettings . coverAspectRatio
2024-06-27 00:03:12 +02:00
mediaItemShare . playbackSession = newPlaybackSession . toJSONForClient ( )
2024-06-29 22:05:35 +02:00
ShareManager . addOpenSharePlaybackSession ( newPlaybackSession )
// 30 day cookie
res . cookie ( 'share_session_id' , newPlaybackSession . shareSessionId , { maxAge : 1000 * 60 * 60 * 24 * 30 , httpOnly : true } )
2024-06-27 00:03:12 +02:00
2024-06-22 23:42:13 +02:00
res . json ( mediaItemShare )
} catch ( error ) {
Logger . error ( ` [ShareController] Failed ` , error )
res . status ( 500 ) . send ( 'Internal server error' )
}
}
2024-06-27 00:03:12 +02:00
/ * *
2024-06-29 22:05:35 +02:00
* Public route - requires share _session _id cookie
*
* GET : / a p i / s h a r e / : s l u g / c o v e r
* Get media item share cover image
2024-06-27 00:03:12 +02:00
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
2024-06-29 22:05:35 +02:00
async getMediaItemShareCoverImage ( req , res ) {
if ( ! req . cookies . share _session _id ) {
return res . status ( 404 ) . send ( 'Share session not set' )
}
const { slug } = req . params
2024-06-27 00:03:12 +02:00
const mediaItemShare = ShareManager . findBySlug ( slug )
if ( ! mediaItemShare ) {
return res . status ( 404 )
}
2024-06-29 22:05:35 +02:00
const playbackSession = ShareManager . findPlaybackSessionBySessionId ( req . cookies . share _session _id )
if ( ! playbackSession || playbackSession . mediaItemShareId !== mediaItemShare . id ) {
return res . status ( 404 ) . send ( 'Share session not found' )
}
const coverPath = playbackSession . coverPath
if ( ! coverPath ) {
return res . status ( 404 ) . send ( 'Cover image not found' )
}
if ( global . XAccel ) {
const encodedURI = encodeUriPath ( global . XAccel + coverPath )
Logger . debug ( ` Use X-Accel to serve static file ${ encodedURI } ` )
return res . status ( 204 ) . header ( { 'X-Accel-Redirect' : encodedURI } ) . send ( )
}
res . sendFile ( coverPath )
}
/ * *
* Public route - requires share _session _id cookie
*
* GET : / a p i / s h a r e / : s l u g / t r a c k / : i n d e x
* Get media item share audio track
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async getMediaItemShareAudioTrack ( req , res ) {
if ( ! req . cookies . share _session _id ) {
return res . status ( 404 ) . send ( 'Share session not set' )
}
2024-06-27 00:03:12 +02:00
2024-06-29 22:05:35 +02:00
const { slug , index } = req . params
const mediaItemShare = ShareManager . findBySlug ( slug )
if ( ! mediaItemShare ) {
return res . status ( 404 )
}
const playbackSession = ShareManager . findPlaybackSessionBySessionId ( req . cookies . share _session _id )
if ( ! playbackSession || playbackSession . mediaItemShareId !== mediaItemShare . id ) {
return res . status ( 404 ) . send ( 'Share session not found' )
}
const audioTrack = playbackSession . audioTracks . find ( ( t ) => t . index === parseInt ( index ) )
if ( ! audioTrack ) {
return res . status ( 404 ) . send ( 'Track not found' )
2024-06-27 00:03:12 +02:00
}
2024-06-29 22:05:35 +02:00
const audioTrackPath = audioTrack . metadata . path
2024-06-27 00:03:12 +02:00
if ( global . XAccel ) {
2024-06-29 22:05:35 +02:00
const encodedURI = encodeUriPath ( global . XAccel + audioTrackPath )
2024-06-27 00:03:12 +02:00
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
2024-06-29 22:05:35 +02:00
const audioMimeType = getAudioMimeTypeFromExtname ( Path . extname ( audioTrackPath ) )
2024-06-27 00:03:12 +02:00
if ( audioMimeType ) {
res . setHeader ( 'Content-Type' , audioMimeType )
}
2024-06-29 22:05:35 +02:00
res . sendFile ( audioTrackPath )
}
/ * *
* Public route - requires share _session _id cookie
*
* PATCH : / a p i / s h a r e / : s l u g / p r o g r e s s
* Update media item share progress
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async updateMediaItemShareProgress ( req , res ) {
if ( ! req . cookies . share _session _id ) {
return res . status ( 404 ) . send ( 'Share session not set' )
}
const { slug } = req . params
const { currentTime } = req . body
if ( currentTime === null || isNaN ( currentTime ) || currentTime < 0 ) {
return res . status ( 400 ) . send ( 'Invalid current time' )
}
const mediaItemShare = ShareManager . findBySlug ( slug )
if ( ! mediaItemShare ) {
return res . status ( 404 )
}
const playbackSession = ShareManager . findPlaybackSessionBySessionId ( req . cookies . share _session _id )
if ( ! playbackSession || playbackSession . mediaItemShareId !== mediaItemShare . id ) {
return res . status ( 404 ) . send ( 'Share session not found' )
}
playbackSession . currentTime = Math . min ( currentTime , playbackSession . duration )
Logger . debug ( ` [ShareController] Update share playback session ${ req . cookies . share _session _id } currentTime: ${ playbackSession . currentTime } ` )
res . sendStatus ( 204 )
2024-06-27 00:03:12 +02:00
}
2024-06-22 23:42:13 +02:00
/ * *
* POST : / a p i / s h a r e / m e d i a i t e m
* Create a new media item share
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async createMediaItemShare ( req , res ) {
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [ShareController] Non-admin user " ${ req . user . username } " attempted to create item share ` )
return res . sendStatus ( 403 )
}
const { slug , expiresAt , mediaItemType , mediaItemId } = req . body
if ( ! slug ? . trim ? . ( ) || typeof mediaItemType !== 'string' || typeof mediaItemId !== 'string' ) {
return res . status ( 400 ) . send ( 'Missing or invalid required fields' )
}
if ( expiresAt === null || isNaN ( expiresAt ) || expiresAt < 0 ) {
return res . status ( 400 ) . send ( 'Invalid expiration date' )
}
if ( ! [ 'book' , 'podcastEpisode' ] . includes ( mediaItemType ) ) {
return res . status ( 400 ) . send ( 'Invalid media item type' )
}
try {
// Check if the media item share already exists by slug or mediaItemId
2024-06-27 00:03:12 +02:00
const existingMediaItemShare = await Database . mediaItemShareModel . findOne ( {
2024-06-22 23:42:13 +02:00
where : {
[ Op . or ] : [ { slug } , { mediaItemId } ]
}
} )
if ( existingMediaItemShare ) {
if ( existingMediaItemShare . mediaItemId === mediaItemId ) {
return res . status ( 409 ) . send ( 'Item is already shared' )
} else {
return res . status ( 409 ) . send ( 'Slug is already in use' )
}
}
// Check that media item exists
const mediaItemModel = mediaItemType === 'book' ? Database . bookModel : Database . podcastEpisodeModel
const mediaItem = await mediaItemModel . findByPk ( mediaItemId )
if ( ! mediaItem ) {
return res . status ( 404 ) . send ( 'Media item not found' )
}
2024-06-27 00:03:12 +02:00
const mediaItemShare = await Database . mediaItemShareModel . create ( {
2024-06-22 23:42:13 +02:00
slug ,
expiresAt : expiresAt || null ,
mediaItemId ,
mediaItemType ,
userId : req . user . id
} )
ShareManager . openMediaItemShare ( mediaItemShare )
res . status ( 201 ) . json ( mediaItemShare ? . toJSONForClient ( ) )
} catch ( error ) {
Logger . error ( ` [ShareController] Failed ` , error )
res . status ( 500 ) . send ( 'Internal server error' )
}
}
/ * *
* DELETE : / a p i / s h a r e / m e d i a i t e m / : i d
* Delete media item share
*
* @ param { import ( 'express' ) . Request } req
* @ param { import ( 'express' ) . Response } res
* /
async deleteMediaItemShare ( req , res ) {
if ( ! req . user . isAdminOrUp ) {
Logger . error ( ` [ShareController] Non-admin user " ${ req . user . username } " attempted to delete item share ` )
return res . sendStatus ( 403 )
}
try {
2024-06-27 00:03:12 +02:00
const mediaItemShare = await Database . mediaItemShareModel . findByPk ( req . params . id )
2024-06-22 23:42:13 +02:00
if ( ! mediaItemShare ) {
return res . status ( 404 ) . send ( 'Media item share not found' )
}
ShareManager . removeMediaItemShare ( mediaItemShare . id )
await mediaItemShare . destroy ( )
res . sendStatus ( 204 )
} catch ( error ) {
Logger . error ( ` [ShareController] Failed ` , error )
res . status ( 500 ) . send ( 'Internal server error' )
}
}
}
module . exports = new ShareController ( )