2022-03-16 01:28:54 +01:00
|
|
|
const Path = require('path')
|
2022-05-12 00:35:04 +02:00
|
|
|
const date = require('date-and-time')
|
2022-03-20 22:41:06 +01:00
|
|
|
const { PlayMethod } = require('../utils/constants')
|
|
|
|
const PlaybackSession = require('../objects/PlaybackSession')
|
|
|
|
const Stream = require('../objects/Stream')
|
|
|
|
const Logger = require('../Logger')
|
2022-04-16 19:37:10 +02:00
|
|
|
const fs = require('fs-extra')
|
2022-03-16 00:57:15 +01:00
|
|
|
|
|
|
|
class PlaybackSessionManager {
|
2022-03-16 01:28:54 +01:00
|
|
|
constructor(db, emitter, clientEmitter) {
|
|
|
|
this.db = db
|
|
|
|
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
|
|
|
|
this.emitter = emitter
|
|
|
|
this.clientEmitter = clientEmitter
|
|
|
|
|
|
|
|
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) {
|
|
|
|
var session = this.getSession(sessionId)
|
|
|
|
return session ? session.stream : null
|
2022-03-16 01:28:54 +01:00
|
|
|
}
|
2022-03-16 00:57:15 +01:00
|
|
|
|
2022-03-26 23:41:26 +01:00
|
|
|
async startSessionRequest(user, libraryItem, episodeId, options, res) {
|
|
|
|
const session = await this.startSession(user, 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) {
|
2022-03-26 17:59:34 +01:00
|
|
|
var result = await this.syncSession(user, session, payload)
|
|
|
|
if (result) {
|
|
|
|
res.json(session.toJSONForClient(result.libraryItem))
|
|
|
|
}
|
2022-03-18 01:10:47 +01:00
|
|
|
}
|
|
|
|
|
2022-04-10 00:56:51 +02:00
|
|
|
async syncLocalSessionRequest(user, sessionJson, res) {
|
|
|
|
var 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}"`)
|
|
|
|
return res.sendStatus(200)
|
|
|
|
}
|
2022-04-10 00:56:51 +02:00
|
|
|
|
|
|
|
var session = await this.db.getPlaybackSession(sessionJson.id)
|
|
|
|
if (!session) {
|
|
|
|
// New session from local
|
|
|
|
session = new PlaybackSession(sessionJson)
|
|
|
|
await this.db.insertEntity('session', session)
|
|
|
|
} else {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
var wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId)
|
|
|
|
if (wasUpdated) {
|
|
|
|
await this.db.updateEntity('user', user)
|
|
|
|
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
|
|
|
this.clientEmitter(user.id, 'user_item_progress_updated', {
|
|
|
|
id: itemProgress.id,
|
|
|
|
data: itemProgress.toJSON()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
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-03-26 23:41:26 +01:00
|
|
|
async startSession(user, libraryItem, episodeId, options) {
|
2022-04-23 23:18:34 +02:00
|
|
|
// Close any sessions already open for user
|
|
|
|
var userSessions = this.sessions.filter(playbackSession => playbackSession.userId === user.id)
|
|
|
|
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-03-26 23:41:26 +01:00
|
|
|
var shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId))
|
2022-04-02 18:19:57 +02:00
|
|
|
var mediaPlayer = options.mediaPlayer || 'unknown'
|
2022-03-18 01:10:47 +01:00
|
|
|
|
2022-03-26 23:41:26 +01:00
|
|
|
const userProgress = user.getMediaProgress(libraryItem.id, episodeId)
|
2022-03-18 01:10:47 +01:00
|
|
|
var userStartTime = 0
|
2022-04-15 12:59:42 +02:00
|
|
|
if (userProgress) userStartTime = Number.parseFloat(userProgress.currentTime) || 0
|
2022-03-16 01:28:54 +01:00
|
|
|
const newPlaybackSession = new PlaybackSession()
|
2022-04-02 18:19:57 +02:00
|
|
|
newPlaybackSession.setData(libraryItem, user, mediaPlayer, episodeId)
|
2022-03-18 01:10:47 +01:00
|
|
|
|
|
|
|
var audioTracks = []
|
|
|
|
if (shouldDirectPlay) {
|
2022-03-26 17:59:34 +01:00
|
|
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
2022-04-03 23:01:59 +02:00
|
|
|
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
2022-03-18 01:10:47 +01:00
|
|
|
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
|
|
|
} else {
|
2022-03-26 17:59:34 +01:00
|
|
|
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
2022-03-26 23:41:26 +01:00
|
|
|
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
2022-03-18 01:10:47 +01:00
|
|
|
await stream.generatePlaylist()
|
2022-04-16 19:37:10 +02:00
|
|
|
stream.start() // Start transcode
|
|
|
|
|
2022-03-18 01:10:47 +01:00
|
|
|
audioTracks = [stream.getAudioTrack()]
|
|
|
|
newPlaybackSession.stream = stream
|
|
|
|
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
2022-04-16 19:37:10 +02:00
|
|
|
|
2022-03-18 01:10:47 +01:00
|
|
|
stream.on('closed', () => {
|
|
|
|
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
|
|
|
newPlaybackSession.stream = null
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
newPlaybackSession.currentTime = userStartTime
|
|
|
|
newPlaybackSession.audioTracks = audioTracks
|
|
|
|
|
|
|
|
// Will save on the first sync
|
|
|
|
user.currentSessionId = newPlaybackSession.id
|
|
|
|
|
2022-03-16 01:28:54 +01:00
|
|
|
this.sessions.push(newPlaybackSession)
|
2022-03-18 01:10:47 +01:00
|
|
|
this.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
|
|
|
|
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-03-18 21:31:46 +01:00
|
|
|
var libraryItem = this.db.libraryItems.find(li => li.id === session.libraryItemId)
|
|
|
|
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-03-26 23:41:26 +01:00
|
|
|
var 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-03-26 23:41:26 +01:00
|
|
|
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
2022-03-18 01:10:47 +01:00
|
|
|
this.clientEmitter(user.id, 'user_item_progress_updated', {
|
|
|
|
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}"`)
|
|
|
|
this.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
|
|
|
|
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) {
|
|
|
|
var session = this.sessions.find(s => s.id === sessionId)
|
|
|
|
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 {
|
|
|
|
var streamsInPath = await fs.readdir(this.StreamsPath)
|
|
|
|
for (let i = 0; i < streamsInPath.length; i++) {
|
|
|
|
var streamId = streamsInPath[i]
|
|
|
|
if (streamId.startsWith('play_')) { // Make sure to only remove folders that are a stream
|
|
|
|
var session = this.sessions.find(se => se.id === streamId)
|
|
|
|
if (!session) {
|
|
|
|
var streamPath = Path.join(this.StreamsPath, streamId)
|
|
|
|
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
|
|
|
}
|
|
|
|
module.exports = PlaybackSessionManager
|