2022-03-16 01:28:54 +01:00
const Path = require ( 'path' )
2022-05-27 02:09:46 +02:00
const serverVersion = require ( '../../package.json' ) . version
2022-11-24 22:53:58 +01:00
const Logger = require ( '../Logger' )
const SocketAuthority = require ( '../SocketAuthority' )
const date = require ( '../libs/dateAndTime' )
const fs = require ( '../libs/fsExtra' )
const uaParserJs = require ( '../libs/uaParser' )
const requestIp = require ( '../libs/requestIp' )
2022-03-20 22:41:06 +01:00
const { PlayMethod } = require ( '../utils/constants' )
2022-11-24 22:53:58 +01:00
2022-03-20 22:41:06 +01:00
const PlaybackSession = require ( '../objects/PlaybackSession' )
2022-05-27 02:09:46 +02:00
const DeviceInfo = require ( '../objects/DeviceInfo' )
2022-03-20 22:41:06 +01:00
const Stream = require ( '../objects/Stream' )
2022-03-16 00:57:15 +01:00
2022-05-27 02:09:46 +02:00
2022-03-16 00:57:15 +01:00
class PlaybackSessionManager {
2022-11-24 22:53:58 +01:00
constructor ( db ) {
2022-03-16 01:28:54 +01:00
this . db = db
this . StreamsPath = Path . join ( global . MetadataPath , 'streams' )
this . sessions = [ ]
2022-08-24 01:10:06 +02:00
this . localSessionLock = { }
2022-03-16 01:28:54 +01:00
}
2022-03-18 01:10:47 +01:00
getSession ( sessionId ) {
return this . sessions . find ( s => s . id === sessionId )
}
getUserSession ( userId ) {
return this . sessions . find ( s => s . userId === userId )
}
getStream ( sessionId ) {
2022-12-13 00:52:20 +01:00
const session = this . getSession ( sessionId )
2022-03-18 01:10:47 +01:00
return session ? session . stream : null
2022-03-16 01:28:54 +01:00
}
2022-03-16 00:57:15 +01:00
2022-05-27 02:09:46 +02:00
getDeviceInfo ( req ) {
const ua = uaParserJs ( req . headers [ 'user-agent' ] )
const ip = requestIp . getClientIp ( req )
const clientDeviceInfo = req . body ? req . body . deviceInfo || null : null // From mobile client
const deviceInfo = new DeviceInfo ( )
deviceInfo . setData ( ip , ua , clientDeviceInfo , serverVersion )
return deviceInfo
}
async startSessionRequest ( req , res , episodeId ) {
const deviceInfo = this . getDeviceInfo ( req )
const { user , libraryItem , body : options } = req
const session = await this . startSession ( user , deviceInfo , libraryItem , episodeId , options )
2022-03-26 17:59:34 +01:00
res . json ( session . toJSONForClient ( libraryItem ) )
2022-03-18 01:10:47 +01:00
}
async syncSessionRequest ( user , session , payload , res ) {
2023-01-08 00:33:05 +01:00
if ( await this . syncSession ( user , session , payload ) ) {
res . sendStatus ( 200 )
} else {
res . sendStatus ( 500 )
2022-03-26 17:59:34 +01:00
}
2022-03-18 01:10:47 +01:00
}
2022-04-10 00:56:51 +02:00
async syncLocalSessionRequest ( user , sessionJson , res ) {
2022-08-24 01:10:06 +02:00
if ( this . localSessionLock [ sessionJson . id ] ) {
Logger . debug ( ` [PlaybackSessionManager] syncLocalSessionRequest: Local session is locked and already syncing ` )
2022-11-18 00:00:37 +01:00
return res . status ( 500 ) . send ( 'Local session is locked and already syncing' )
2022-08-24 01:10:06 +02:00
}
2022-12-13 00:52:20 +01:00
const libraryItem = this . db . getLibraryItem ( sessionJson . libraryItemId )
2022-05-12 00:07:41 +02:00
if ( ! libraryItem ) {
Logger . error ( ` [PlaybackSessionManager] syncLocalSessionRequest: Library item not found for session " ${ sessionJson . libraryItemId } " ` )
2022-11-18 00:00:37 +01:00
return res . status ( 500 ) . send ( 'Library item not found' )
2022-05-12 00:07:41 +02:00
}
2022-04-10 00:56:51 +02:00
2022-08-24 01:10:06 +02:00
this . localSessionLock [ sessionJson . id ] = true // Lock local session
2022-12-13 00:52:20 +01:00
let session = await this . db . getPlaybackSession ( sessionJson . id )
2022-04-10 00:56:51 +02:00
if ( ! session ) {
// New session from local
session = new PlaybackSession ( sessionJson )
await this . db . insertEntity ( 'session' , session )
} else {
2022-08-27 02:28:41 +02:00
session . currentTime = sessionJson . currentTime
2022-04-10 00:56:51 +02:00
session . timeListening = sessionJson . timeListening
session . updatedAt = sessionJson . updatedAt
2022-05-12 00:35:04 +02:00
session . date = date . format ( new Date ( ) , 'YYYY-MM-DD' )
session . dayOfWeek = date . format ( new Date ( ) , 'dddd' )
2022-04-10 00:56:51 +02:00
await this . db . updateEntity ( 'session' , session )
}
session . currentTime = sessionJson . currentTime
const itemProgressUpdate = {
duration : session . duration ,
currentTime : session . currentTime ,
progress : session . progress ,
lastUpdate : session . updatedAt // Keep media progress update times the same as local
}
2022-12-13 00:52:20 +01:00
const wasUpdated = user . createUpdateMediaProgress ( libraryItem , itemProgressUpdate , session . episodeId )
2022-04-10 00:56:51 +02:00
if ( wasUpdated ) {
await this . db . updateEntity ( 'user' , user )
2022-12-13 00:52:20 +01:00
const itemProgress = user . getMediaProgress ( session . libraryItemId , session . episodeId )
2022-11-24 22:53:58 +01:00
SocketAuthority . clientEmitter ( user . id , 'user_item_progress_updated' , {
2022-04-10 00:56:51 +02:00
id : itemProgress . id ,
data : itemProgress . toJSON ( )
} )
}
2022-08-24 01:10:06 +02:00
delete this . localSessionLock [ sessionJson . id ] // Unlock local session
2022-04-10 00:56:51 +02:00
res . sendStatus ( 200 )
}
2022-03-18 01:10:47 +01:00
async closeSessionRequest ( user , session , syncData , res ) {
await this . closeSession ( user , session , syncData )
res . sendStatus ( 200 )
}
2022-03-17 01:15:25 +01:00
2022-05-27 02:09:46 +02:00
async startSession ( user , deviceInfo , libraryItem , episodeId , options ) {
2022-04-23 23:18:34 +02:00
// Close any sessions already open for user
2022-12-13 00:18:56 +01:00
const userSessions = this . sessions . filter ( playbackSession => playbackSession . userId === user . id )
2022-04-23 23:18:34 +02:00
for ( const session of userSessions ) {
Logger . info ( ` [PlaybackSessionManager] startSession: Closing open session " ${ session . displayTitle } " for user " ${ user . username } " ` )
await this . closeSession ( user , session , null )
}
2022-12-13 00:18:56 +01:00
const shouldDirectPlay = options . forceDirectPlay || ( ! options . forceTranscode && libraryItem . media . checkCanDirectPlay ( options , episodeId ) )
const mediaPlayer = options . mediaPlayer || 'unknown'
2022-03-18 01:10:47 +01:00
2022-12-22 23:38:55 +01:00
const userProgress = libraryItem . isMusic ? null : user . getMediaProgress ( libraryItem . id , episodeId )
2022-12-13 00:18:56 +01:00
let userStartTime = 0
if ( userProgress ) {
if ( userProgress . isFinished ) {
Logger . info ( ` [PlaybackSessionManager] Starting session for user " ${ user . username } " and resetting progress for finished item " ${ libraryItem . media . metadata . title } " ` )
// Keep userStartTime as 0 so the client restarts the media
} else {
userStartTime = Number . parseFloat ( userProgress . currentTime ) || 0
}
}
2022-03-16 01:28:54 +01:00
const newPlaybackSession = new PlaybackSession ( )
2022-05-27 02:09:46 +02:00
newPlaybackSession . setData ( libraryItem , user , mediaPlayer , deviceInfo , userStartTime , episodeId )
2022-03-18 01:10:47 +01:00
2022-05-31 02:26:53 +02:00
if ( libraryItem . mediaType === 'video' ) {
if ( shouldDirectPlay ) {
Logger . debug ( ` [PlaybackSessionManager] " ${ user . username } " starting direct play session for item " ${ libraryItem . id } " ` )
newPlaybackSession . videoTrack = libraryItem . media . getVideoTrack ( )
newPlaybackSession . playMethod = PlayMethod . DIRECTPLAY
} else {
// HLS not supported for video yet
}
2022-03-18 01:10:47 +01:00
} else {
2022-12-13 00:52:20 +01:00
let audioTracks = [ ]
2022-05-31 02:26:53 +02:00
if ( shouldDirectPlay ) {
Logger . debug ( ` [PlaybackSessionManager] " ${ user . username } " starting direct play session for item " ${ libraryItem . id } " ` )
audioTracks = libraryItem . getDirectPlayTracklist ( episodeId )
newPlaybackSession . playMethod = PlayMethod . DIRECTPLAY
} else {
Logger . debug ( ` [PlaybackSessionManager] " ${ user . username } " starting stream session for item " ${ libraryItem . id } " ` )
2022-12-13 00:52:20 +01:00
const stream = new Stream ( newPlaybackSession . id , this . StreamsPath , user , libraryItem , episodeId , userStartTime )
2022-05-31 02:26:53 +02:00
await stream . generatePlaylist ( )
stream . start ( ) // Start transcode
audioTracks = [ stream . getAudioTrack ( ) ]
newPlaybackSession . stream = stream
newPlaybackSession . playMethod = PlayMethod . TRANSCODE
stream . on ( 'closed' , ( ) => {
Logger . debug ( ` [PlaybackSessionManager] Stream closed for session " ${ newPlaybackSession . id } " ` )
newPlaybackSession . stream = null
} )
}
newPlaybackSession . audioTracks = audioTracks
2022-03-18 01:10:47 +01:00
}
// Will save on the first sync
user . currentSessionId = newPlaybackSession . id
2022-03-16 01:28:54 +01:00
this . sessions . push ( newPlaybackSession )
2022-11-24 23:35:26 +01:00
SocketAuthority . adminEmitter ( 'user_stream_update' , user . toJSONForPublic ( this . sessions , this . db . libraryItems ) )
2022-03-18 01:10:47 +01:00
2022-03-16 01:28:54 +01:00
return newPlaybackSession
2022-03-16 00:57:15 +01:00
}
2022-03-18 01:10:47 +01:00
async syncSession ( user , session , syncData ) {
2022-12-13 00:52:20 +01:00
const libraryItem = this . db . libraryItems . find ( li => li . id === session . libraryItemId )
2022-03-18 21:31:46 +01:00
if ( ! libraryItem ) {
2022-03-26 23:41:26 +01:00
Logger . error ( ` [PlaybackSessionManager] syncSession Library Item not found " ${ session . libraryItemId } " ` )
2022-03-26 17:59:34 +01:00
return null
2022-03-18 21:31:46 +01:00
}
2022-03-18 01:10:47 +01:00
session . currentTime = syncData . currentTime
session . addListeningTime ( syncData . timeListened )
Logger . debug ( ` [PlaybackSessionManager] syncSession " ${ session . id } " | Total Time Listened: ${ session . timeListening } ` )
const itemProgressUpdate = {
2022-03-18 21:31:46 +01:00
duration : syncData . duration ,
2022-03-18 01:10:47 +01:00
currentTime : syncData . currentTime ,
progress : session . progress
}
2022-12-13 00:52:20 +01:00
const wasUpdated = user . createUpdateMediaProgress ( libraryItem , itemProgressUpdate , session . episodeId )
2022-03-18 01:10:47 +01:00
if ( wasUpdated ) {
2022-03-26 23:41:26 +01:00
2022-03-18 01:10:47 +01:00
await this . db . updateEntity ( 'user' , user )
2022-12-13 00:52:20 +01:00
const itemProgress = user . getMediaProgress ( session . libraryItemId , session . episodeId )
2022-11-24 22:53:58 +01:00
SocketAuthority . clientEmitter ( user . id , 'user_item_progress_updated' , {
2022-03-18 01:10:47 +01:00
id : itemProgress . id ,
data : itemProgress . toJSON ( )
} )
}
this . saveSession ( session )
2022-03-26 17:59:34 +01:00
return {
libraryItem
}
2022-03-18 01:10:47 +01:00
}
async closeSession ( user , session , syncData = null ) {
if ( syncData ) {
await this . syncSession ( user , session , syncData )
} else {
await this . saveSession ( session )
}
Logger . debug ( ` [PlaybackSessionManager] closeSession " ${ session . id } " ` )
2022-11-24 23:35:26 +01:00
SocketAuthority . adminEmitter ( 'user_stream_update' , user . toJSONForPublic ( this . sessions , this . db . libraryItems ) )
2022-03-18 01:10:47 +01:00
return this . removeSession ( session . id )
}
saveSession ( session ) {
2022-03-18 21:31:46 +01:00
if ( ! session . timeListening ) return // Do not save a session with no listening time
2022-03-18 01:10:47 +01:00
if ( session . lastSave ) {
return this . db . updateEntity ( 'session' , session )
} else {
session . lastSave = Date . now ( )
return this . db . insertEntity ( 'session' , session )
}
}
async removeSession ( sessionId ) {
2022-12-13 00:52:20 +01:00
const session = this . sessions . find ( s => s . id === sessionId )
2022-03-18 01:10:47 +01:00
if ( ! session ) return
if ( session . stream ) {
await session . stream . close ( )
}
this . sessions = this . sessions . filter ( s => s . id !== sessionId )
Logger . debug ( ` [PlaybackSessionManager] Removed session " ${ sessionId } " ` )
}
2022-04-16 19:37:10 +02:00
// Check for streams that are not in memory and remove
async removeOrphanStreams ( ) {
await fs . ensureDir ( this . StreamsPath )
try {
2022-12-13 00:52:20 +01:00
const streamsInPath = await fs . readdir ( this . StreamsPath )
2022-04-16 19:37:10 +02:00
for ( let i = 0 ; i < streamsInPath . length ; i ++ ) {
2022-12-13 00:52:20 +01:00
const streamId = streamsInPath [ i ]
2022-04-16 19:37:10 +02:00
if ( streamId . startsWith ( 'play_' ) ) { // Make sure to only remove folders that are a stream
2022-12-13 00:52:20 +01:00
const session = this . sessions . find ( se => se . id === streamId )
2022-04-16 19:37:10 +02:00
if ( ! session ) {
2022-12-13 00:52:20 +01:00
const streamPath = Path . join ( this . StreamsPath , streamId )
2022-04-16 19:37:10 +02:00
Logger . debug ( ` [PlaybackSessionManager] Removing orphan stream " ${ streamPath } " ` )
await fs . remove ( streamPath )
}
}
}
} catch ( error ) {
Logger . error ( ` [PlaybackSessionManager] cleanOrphanStreams failed ` , error )
}
}
2022-07-30 00:13:46 +02:00
// Android app v0.9.54 and below had a bug where listening time was sending unix timestamp
// See https://github.com/advplyr/audiobookshelf/issues/868
// Remove playback sessions with listening time too high
async removeInvalidSessions ( ) {
const selectFunc = ( session ) => isNaN ( session . timeListening ) || Number ( session . timeListening ) > 3600000000
const numSessionsRemoved = await this . db . removeEntities ( 'session' , selectFunc )
if ( numSessionsRemoved ) {
Logger . info ( ` [PlaybackSessionManager] Removed ${ numSessionsRemoved } invalid playback sessions ` )
}
}
2022-03-16 00:57:15 +01:00
}
2022-08-27 02:28:41 +02:00
module . exports = PlaybackSessionManager