2023-07-16 22:05:51 +02:00
const uuidv4 = require ( "uuid" ) . v4
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' )
2023-07-05 01:14:44 +02:00
const Database = require ( '../Database' )
2022-11-24 22:53:58 +01:00
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 {
2023-07-05 01:14:44 +02:00
constructor ( ) {
2022-03-16 01:28:54 +01:00
this . StreamsPath = Path . join ( global . MetadataPath , 'streams' )
2023-07-16 22:05:51 +02:00
this . oldPlaybackSessionMap = { } // TODO: Remove after updated mobile versions
2022-03-16 01:28:54 +01:00
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
2023-07-05 01:14:44 +02:00
async getDeviceInfo ( req ) {
2022-05-27 02:09:46 +02:00
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 ( )
2023-07-05 01:14:44 +02:00
deviceInfo . setData ( ip , ua , clientDeviceInfo , serverVersion , req . user . id )
if ( clientDeviceInfo ? . deviceId ) {
const existingDevice = await Database . getDeviceByDeviceId ( clientDeviceInfo . deviceId )
if ( existingDevice ) {
if ( existingDevice . update ( deviceInfo ) ) {
await Database . updateDevice ( existingDevice )
}
return existingDevice
}
}
2023-07-09 18:39:15 +02:00
await Database . createDevice ( deviceInfo )
2023-07-05 01:14:44 +02:00
2022-05-27 02:09:46 +02:00
return deviceInfo
}
async startSessionRequest ( req , res , episodeId ) {
2023-07-05 01:14:44 +02:00
const deviceInfo = await 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 ) {
2023-07-16 22:05:51 +02:00
const deviceInfo = await this . getDeviceInfo ( req )
2023-02-05 23:52:17 +01:00
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 } ) ` )
2023-07-16 22:05:51 +02:00
const result = await this . syncLocalSession ( user , sessionJson , deviceInfo )
2023-02-05 23:52:17 +01:00
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-07-16 22:05:51 +02:00
async syncLocalSession ( user , sessionJson , deviceInfo ) {
2023-09-04 23:33:55 +02:00
const libraryItem = await Database . libraryItemModel . getOldById ( sessionJson . libraryItemId )
2023-02-05 23:52:17 +01:00
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
2023-07-17 20:58:19 +02:00
sessionJson . userId = user . id
sessionJson . serverVersion = serverVersion
2023-07-16 22:05:51 +02:00
// TODO: Temp update local playback session id to uuidv4 & library item/book/episode ids
if ( sessionJson . id ? . startsWith ( 'play_local_' ) ) {
if ( ! this . oldPlaybackSessionMap [ sessionJson . id ] ) {
const newSessionId = uuidv4 ( )
this . oldPlaybackSessionMap [ sessionJson . id ] = newSessionId
sessionJson . id = newSessionId
} else {
sessionJson . id = this . oldPlaybackSessionMap [ sessionJson . id ]
}
}
if ( sessionJson . libraryItemId !== libraryItem . id ) {
Logger . info ( ` [PlaybackSessionManager] Mapped old libraryItemId " ${ sessionJson . libraryItemId } " to ${ libraryItem . id } ` )
sessionJson . libraryItemId = libraryItem . id
sessionJson . bookId = episode ? null : libraryItem . media . id
}
if ( ! sessionJson . bookId && ! episode ) {
sessionJson . bookId = libraryItem . media . id
}
if ( episode && sessionJson . episodeId !== episode . id ) {
Logger . info ( ` [PlaybackSessionManager] Mapped old episodeId " ${ sessionJson . episodeId } " to ${ episode . id } ` )
sessionJson . episodeId = episode . id
}
if ( sessionJson . libraryId !== libraryItem . libraryId ) {
sessionJson . libraryId = libraryItem . libraryId
}
2023-07-05 01:14:44 +02:00
let session = await Database . getPlaybackSession ( sessionJson . id )
2022-04-10 00:56:51 +02:00
if ( ! session ) {
// New session from local
session = new PlaybackSession ( sessionJson )
2023-07-16 22:05:51 +02:00
session . deviceInfo = deviceInfo
2023-02-05 23:52:17 +01:00
Logger . debug ( ` [PlaybackSessionManager] Inserting new session for " ${ session . displayTitle } " ( ${ session . id } ) ` )
2023-07-05 01:14:44 +02:00
await Database . createPlaybackSession ( session )
2022-04-10 00:56:51 +02:00
} 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
2024-03-30 17:40:35 +01:00
let jsDate = new Date ( sessionJson . updatedAt )
if ( isNaN ( jsDate ) ) {
jsDate = new Date ( )
}
session . date = date . format ( jsDate , 'YYYY-MM-DD' )
session . dayOfWeek = date . format ( jsDate , 'dddd' )
2023-02-05 23:52:17 +01:00
Logger . debug ( ` [PlaybackSessionManager] Updated session for " ${ session . displayTitle } " ( ${ session . id } ) ` )
2023-07-05 01:14:44 +02:00
await Database . updatePlaybackSession ( session )
2022-04-10 00:56:51 +02:00
}
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-12-13 00:52:20 +01:00
const itemProgress = user . getMediaProgress ( session . libraryItemId , session . episodeId )
2023-07-05 01:14:44 +02:00
if ( itemProgress ) await Database . upsertMediaProgress ( itemProgress )
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-07-16 22:05:51 +02:00
async syncLocalSessionRequest ( req , res ) {
const deviceInfo = await this . getDeviceInfo ( req )
const user = req . user
const sessionJson = req . body
const result = await this . syncLocalSession ( user , sessionJson , deviceInfo )
2023-02-05 23:52:17 +01:00
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
2023-07-05 01:14:44 +02:00
const userSessions = this . sessions . filter ( playbackSession => playbackSession . userId === user . id && playbackSession . deviceId === deviceInfo . id )
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
}
2022-03-16 01:28:54 +01:00
this . sessions . push ( newPlaybackSession )
2023-08-12 23:11:58 +02:00
SocketAuthority . adminEmitter ( 'user_stream_update' , user . toJSONForPublic ( this . sessions ) )
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 ) {
2023-08-20 20:34:03 +02:00
const libraryItem = await Database . libraryItemModel . getOldById ( 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-12-13 00:52:20 +01:00
const itemProgress = user . getMediaProgress ( session . libraryItemId , session . episodeId )
2023-07-05 01:14:44 +02:00
if ( itemProgress ) await Database . upsertMediaProgress ( itemProgress )
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 } " ` )
2023-08-12 23:11:58 +02:00
SocketAuthority . adminEmitter ( 'user_stream_update' , user . toJSONForPublic ( this . sessions ) )
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 ) {
2023-07-05 01:14:44 +02:00
return Database . updatePlaybackSession ( session )
2022-03-18 01:10:47 +01:00
} else {
session . lastSave = Date . now ( )
2023-07-05 01:14:44 +02:00
return Database . createPlaybackSession ( session )
2022-03-18 01:10:47 +01:00
}
}
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
2023-10-12 00:05:56 +02:00
/ * *
* Remove all stream folders in ` /metadata/streams `
* /
2022-04-16 19:37:10 +02:00
async removeOrphanStreams ( ) {
await fs . ensureDir ( this . StreamsPath )
try {
2022-12-13 00:52:20 +01:00
const streamsInPath = await fs . readdir ( this . StreamsPath )
2023-10-12 00:05:56 +02:00
for ( const streamId of streamsInPath ) {
if ( /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/ . test ( streamId ) ) { // Ensure is uuidv4
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-03-16 00:57:15 +01:00
}
2022-08-27 02:28:41 +02:00
module . exports = PlaybackSessionManager