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
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-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 )
2023-04-09 01:01:24 +02:00
return 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 )
2023-04-09 01:01:24 +02:00
const clientDeviceInfo = req . body ? . deviceInfo || null
2022-05-27 02:09:46 +02:00
const deviceInfo = new DeviceInfo ( )
deviceInfo . setData ( ip , ua , clientDeviceInfo , serverVersion )
return deviceInfo
}
async startSessionRequest ( req , res , episodeId ) {
const deviceInfo = this . getDeviceInfo ( req )
2023-02-04 20:23:13 +01:00
Logger . debug ( ` [PlaybackSessionManager] startSessionRequest for device ${ deviceInfo . deviceDescription } ` )
2022-05-27 02:09:46 +02:00
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
}
2023-02-05 23:52:17 +01:00
async syncLocalSessionsRequest ( req , res ) {
const user = req . user
const sessions = req . body . sessions || [ ]
2022-08-24 01:10:06 +02:00
2023-02-05 23:52:17 +01:00
const syncResults = [ ]
for ( const sessionJson of sessions ) {
Logger . info ( ` [PlaybackSessionManager] Syncing local session " ${ sessionJson . displayTitle } " ( ${ sessionJson . id } ) ` )
const result = await this . syncLocalSession ( user , sessionJson )
syncResults . push ( result )
2022-05-12 00:07:41 +02:00
}
2022-04-10 00:56:51 +02:00
2023-02-05 23:52:17 +01:00
res . json ( {
results : syncResults
2023-01-15 22:00:18 +01:00
} )
2023-02-05 23:52:17 +01:00
}
2023-01-15 22:00:18 +01:00
2023-02-05 23:52:17 +01:00
async syncLocalSession ( user , sessionJson ) {
const libraryItem = this . db . getLibraryItem ( sessionJson . libraryItemId )
const episode = ( sessionJson . episodeId && libraryItem && libraryItem . isPodcast ) ? libraryItem . media . getEpisode ( sessionJson . episodeId ) : null
if ( ! libraryItem || ( libraryItem . isPodcast && ! episode ) ) {
Logger . error ( ` [PlaybackSessionManager] syncLocalSession: Media item not found for session " ${ sessionJson . displayTitle } " ( ${ sessionJson . id } ) ` )
return {
id : sessionJson . id ,
success : false ,
error : 'Media item not found'
}
}
2022-08-24 01:10:06 +02:00
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 )
2023-02-05 23:52:17 +01:00
Logger . debug ( ` [PlaybackSessionManager] Inserting new session for " ${ session . displayTitle } " ( ${ session . id } ) ` )
2022-04-10 00:56:51 +02:00
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' )
2023-02-05 23:52:17 +01:00
Logger . debug ( ` [PlaybackSessionManager] Updated session for " ${ session . displayTitle } " ( ${ session . id } ) ` )
2022-04-10 00:56:51 +02:00
await this . db . updateEntity ( 'session' , session )
}
2023-02-05 23:52:17 +01:00
const result = {
id : session . id ,
success : true ,
progressSynced : false
}
2022-04-10 00:56:51 +02:00
2023-02-05 23:52:17 +01:00
const userProgressForItem = user . getMediaProgress ( session . libraryItemId , session . episodeId )
if ( userProgressForItem ) {
if ( userProgressForItem . lastUpdate > session . updatedAt ) {
Logger . debug ( ` [PlaybackSessionManager] Not updating progress for " ${ session . displayTitle } " because it has been updated more recently ` )
} else {
Logger . debug ( ` [PlaybackSessionManager] Updating progress for " ${ session . displayTitle } " with current time ${ session . currentTime } (previously ${ userProgressForItem . currentTime } ) ` )
result . progressSynced = user . createUpdateMediaProgress ( libraryItem , session . mediaProgressObject , session . episodeId )
}
} else {
Logger . debug ( ` [PlaybackSessionManager] Creating new media progress for media item " ${ session . displayTitle } " ` )
result . progressSynced = user . createUpdateMediaProgress ( libraryItem , session . mediaProgressObject , session . episodeId )
2022-04-10 00:56:51 +02:00
}
2023-02-05 23:52:17 +01:00
// Update user and emit socket event
if ( result . progressSynced ) {
2022-04-10 00:56:51 +02: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-04-10 00:56:51 +02:00
id : itemProgress . id ,
2023-05-28 00:21:43 +02:00
sessionId : session . id ,
deviceDescription : session . deviceDescription ,
2022-04-10 00:56:51 +02:00
data : itemProgress . toJSON ( )
} )
}
2022-08-24 01:10:06 +02:00
2023-02-05 23:52:17 +01:00
return result
}
2022-08-24 01:10:06 +02:00
2023-02-05 23:52:17 +01:00
async syncLocalSessionRequest ( user , sessionJson , res ) {
const result = await this . syncLocalSession ( user , sessionJson )
if ( result . error ) {
res . status ( 500 ) . send ( result . error )
} else {
res . sendStatus ( 200 )
}
2022-04-10 00:56:51 +02:00
}
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 ) {
2023-04-09 01:01:24 +02:00
// Close any sessions already open for user and device
const userSessions = this . sessions . filter ( playbackSession => playbackSession . userId === user . id && playbackSession . deviceId === deviceInfo . deviceId )
2022-04-23 23:18:34 +02:00
for ( const session of userSessions ) {
2023-02-04 20:23:13 +01:00
Logger . info ( ` [PlaybackSessionManager] startSession: Closing open session " ${ session . displayTitle } " for user " ${ user . username } " (Device: ${ session . deviceDescription } ) ` )
2022-04-23 23:18:34 +02:00
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 ) {
2023-02-02 23:24:34 +01:00
Logger . debug ( ` [PlaybackSessionManager] " ${ user . username } " starting direct play session for item " ${ libraryItem . id } " with id ${ newPlaybackSession . id } ` )
2022-05-31 02:26:53 +02:00
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 ) {
2023-02-04 20:23:13 +01:00
Logger . debug ( ` [PlaybackSessionManager] " ${ user . username } " starting direct play session for item " ${ libraryItem . id } " with id ${ newPlaybackSession . id } (Device: ${ newPlaybackSession . deviceDescription } ) ` )
2022-05-31 02:26:53 +02:00
audioTracks = libraryItem . getDirectPlayTracklist ( episodeId )
newPlaybackSession . playMethod = PlayMethod . DIRECTPLAY
} else {
2023-02-04 20:23:13 +01:00
Logger . debug ( ` [PlaybackSessionManager] " ${ user . username } " starting stream session for item " ${ libraryItem . id } " (Device: ${ newPlaybackSession . deviceDescription } ) ` )
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' , ( ) => {
2023-02-04 20:23:13 +01:00
Logger . debug ( ` [PlaybackSessionManager] Stream closed for session " ${ newPlaybackSession . id } " (Device: ${ newPlaybackSession . deviceDescription } ) ` )
2022-05-31 02:26:53 +02:00
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 )
2023-02-04 20:23:13 +01:00
Logger . debug ( ` [PlaybackSessionManager] syncSession " ${ session . id } " (Device: ${ session . deviceDescription } ) | Total Time Listened: ${ session . timeListening } ` )
2022-03-18 01:10:47 +01:00
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 ,
2023-05-28 00:21:43 +02:00
sessionId : session . id ,
deviceDescription : session . deviceDescription ,
2022-03-18 01:10:47 +01:00
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 ) )
2023-04-09 01:01:24 +02:00
SocketAuthority . clientEmitter ( session . userId , 'user_session_closed' , session . id )
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 ( ) {
2023-05-28 00:21:43 +02:00
const selectFunc = ( session ) => isNaN ( session . timeListening ) || Number ( session . timeListening ) > 36000000
2023-01-08 16:15:11 +01:00
const numSessionsRemoved = await this . db . removeEntities ( 'session' , selectFunc , true )
2022-07-30 00:13:46 +02:00
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