2021-09-04 21:17:26 +02:00
|
|
|
const Stream = require('./objects/Stream')
|
2021-09-18 23:42:20 +02:00
|
|
|
// const StreamTest = require('./test/StreamTest')
|
2021-08-18 00:01:11 +02:00
|
|
|
const Logger = require('./Logger')
|
|
|
|
const fs = require('fs-extra')
|
|
|
|
const Path = require('path')
|
|
|
|
|
|
|
|
class StreamManager {
|
2021-10-26 04:14:54 +02:00
|
|
|
constructor(db, MetadataPath, emitter, clientEmitter) {
|
2021-08-18 00:01:11 +02:00
|
|
|
this.db = db
|
|
|
|
|
2021-10-23 03:08:02 +02:00
|
|
|
this.emitter = emitter
|
2021-10-26 04:14:54 +02:00
|
|
|
this.clientEmitter = clientEmitter
|
2021-10-23 03:08:02 +02:00
|
|
|
|
2021-09-22 03:57:33 +02:00
|
|
|
this.MetadataPath = MetadataPath
|
2021-08-18 00:01:11 +02:00
|
|
|
this.streams = []
|
2021-09-22 03:57:33 +02:00
|
|
|
this.StreamsPath = Path.join(this.MetadataPath, 'streams')
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
get audiobooks() {
|
|
|
|
return this.db.audiobooks
|
|
|
|
}
|
|
|
|
|
|
|
|
getStream(streamId) {
|
|
|
|
return this.streams.find(s => s.id === streamId)
|
|
|
|
}
|
|
|
|
|
|
|
|
removeStream(stream) {
|
|
|
|
this.streams = this.streams.filter(s => s.id !== stream.id)
|
|
|
|
}
|
|
|
|
|
2021-11-13 22:24:56 +01:00
|
|
|
async openStream(client, audiobook, transcodeOptions = {}) {
|
|
|
|
if (!client || !client.user) {
|
|
|
|
Logger.error('[StreamManager] Cannot open stream invalid client', client)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var stream = new Stream(this.StreamsPath, client, audiobook, transcodeOptions)
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
stream.on('closed', () => {
|
|
|
|
this.removeStream(stream)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.streams.push(stream)
|
|
|
|
|
|
|
|
await stream.generatePlaylist()
|
|
|
|
stream.start()
|
|
|
|
|
|
|
|
Logger.info('Stream Opened for client', client.user.username, 'for audiobook', audiobook.title, 'with streamId', stream.id)
|
|
|
|
|
|
|
|
client.stream = stream
|
|
|
|
client.user.stream = stream.id
|
|
|
|
|
|
|
|
return stream
|
|
|
|
}
|
|
|
|
|
2021-09-22 03:57:33 +02:00
|
|
|
ensureStreamsDir() {
|
|
|
|
return fs.ensureDir(this.StreamsPath)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
removeOrphanStreamFiles(streamId) {
|
|
|
|
try {
|
2021-09-22 03:57:33 +02:00
|
|
|
var StreamsPath = Path.join(this.StreamsPath, streamId)
|
|
|
|
return fs.remove(StreamsPath)
|
2021-08-18 00:01:11 +02:00
|
|
|
} catch (error) {
|
|
|
|
Logger.debug('No orphan stream', streamId)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-22 03:57:33 +02:00
|
|
|
async tempCheckStrayStreams() {
|
|
|
|
try {
|
|
|
|
var dirs = await fs.readdir(this.MetadataPath)
|
|
|
|
if (!dirs || !dirs.length) return true
|
|
|
|
|
|
|
|
await Promise.all(dirs.map(async (dirname) => {
|
2021-12-13 00:15:37 +01:00
|
|
|
if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs' && dirname !== 'cache') {
|
2021-09-22 03:57:33 +02:00
|
|
|
var fullPath = Path.join(this.MetadataPath, dirname)
|
|
|
|
Logger.warn(`Removing OLD Orphan Stream ${dirname}`)
|
|
|
|
return fs.remove(fullPath)
|
|
|
|
}
|
|
|
|
}))
|
2021-09-25 17:35:33 +02:00
|
|
|
|
2021-09-22 03:57:33 +02:00
|
|
|
return true
|
|
|
|
} catch (error) {
|
|
|
|
Logger.debug('No old orphan streams', error)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
async removeOrphanStreams() {
|
2021-09-22 03:57:33 +02:00
|
|
|
await this.tempCheckStrayStreams()
|
2021-08-18 00:01:11 +02:00
|
|
|
try {
|
2021-09-22 03:57:33 +02:00
|
|
|
var dirs = await fs.readdir(this.StreamsPath)
|
2021-08-18 00:01:11 +02:00
|
|
|
if (!dirs || !dirs.length) return true
|
|
|
|
|
|
|
|
await Promise.all(dirs.map(async (dirname) => {
|
2021-09-22 03:57:33 +02:00
|
|
|
var fullPath = Path.join(this.StreamsPath, dirname)
|
2021-08-18 00:01:11 +02:00
|
|
|
Logger.info(`Removing Orphan Stream ${dirname}`)
|
|
|
|
return fs.remove(fullPath)
|
|
|
|
}))
|
|
|
|
return true
|
|
|
|
} catch (error) {
|
2021-09-22 03:57:33 +02:00
|
|
|
Logger.debug('No orphan stream', error)
|
2021-08-18 00:01:11 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 15:39:21 +01:00
|
|
|
async openStreamApiRequest(res, user, audiobook) {
|
|
|
|
Logger.info(`[StreamManager] User "${user.username}" open stream request for "${audiobook.title}"`)
|
|
|
|
var client = {
|
|
|
|
user
|
|
|
|
}
|
|
|
|
var stream = await this.openStream(client, audiobook)
|
|
|
|
this.db.updateUserStream(client.user.id, stream.id)
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
audiobookId: audiobook.id,
|
|
|
|
startTime: stream.startTime,
|
|
|
|
streamId: stream.id,
|
|
|
|
streamUrl: stream.clientPlaylistUri
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
async openStreamSocketRequest(socket, audiobookId) {
|
2021-11-11 15:39:21 +01:00
|
|
|
Logger.info('[StreamManager] Open Stream Request', socket.id, audiobookId)
|
2021-08-18 00:01:11 +02:00
|
|
|
var audiobook = this.audiobooks.find(ab => ab.id === audiobookId)
|
|
|
|
var client = socket.sheepClient
|
|
|
|
|
|
|
|
if (client.stream) {
|
|
|
|
Logger.info('Closing client stream first', client.stream.id)
|
|
|
|
await client.stream.close()
|
|
|
|
client.user.stream = null
|
|
|
|
client.stream = null
|
|
|
|
}
|
|
|
|
|
|
|
|
var stream = await this.openStream(client, audiobook)
|
|
|
|
this.db.updateUserStream(client.user.id, stream.id)
|
2021-10-13 03:07:42 +02:00
|
|
|
|
2021-10-23 03:08:02 +02:00
|
|
|
this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async closeStreamRequest(socket) {
|
|
|
|
Logger.info('Close Stream Request', socket.id)
|
|
|
|
var client = socket.sheepClient
|
|
|
|
if (!client || !client.stream) {
|
2021-09-03 13:40:59 +02:00
|
|
|
Logger.error('No stream for client', (client && client.user) ? client.user.username : 'No Client')
|
2021-09-02 02:39:38 +02:00
|
|
|
client.socket.emit('stream_closed', 'n/a')
|
2021-08-18 00:01:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// var streamId = client.stream.id
|
|
|
|
await client.stream.close()
|
|
|
|
client.user.stream = null
|
|
|
|
client.stream = null
|
|
|
|
this.db.updateUserStream(client.user.id, null)
|
2021-10-13 03:07:42 +02:00
|
|
|
|
2021-10-23 03:08:02 +02:00
|
|
|
this.emitter('user_stream_update', client.user.toJSONForPublic(this.streams))
|
2021-09-01 20:47:18 +02:00
|
|
|
}
|
|
|
|
|
2021-11-13 02:43:16 +01:00
|
|
|
streamSync(socket, syncData) {
|
|
|
|
const client = socket.sheepClient
|
|
|
|
if (!client || !client.stream) {
|
|
|
|
Logger.error('[StreamManager] streamSync: No stream for client', (client && client.user) ? client.user.id : 'No Client')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (client.stream.id !== syncData.streamId) {
|
|
|
|
Logger.error('[StreamManager] streamSync: Stream id mismatch on stream update', syncData.streamId, client.stream.id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (!client.user) {
|
|
|
|
Logger.error('[StreamManager] streamSync: No User for client', client)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// const { timeListened, currentTime, streamId } = syncData
|
|
|
|
var listeningSession = client.stream.syncStream(syncData)
|
|
|
|
|
|
|
|
if (listeningSession && listeningSession.timeListening > 0) {
|
|
|
|
// Save listening session
|
|
|
|
var existingListeningSession = this.db.sessions.find(s => s.id === listeningSession.id)
|
|
|
|
if (existingListeningSession) {
|
|
|
|
this.db.updateEntity('session', listeningSession)
|
|
|
|
} else {
|
|
|
|
this.db.sessions.push(listeningSession.toJSON()) // Insert right away to prevent duplicate session
|
|
|
|
this.db.insertEntity('session', listeningSession)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
|
|
|
|
this.db.updateEntity('user', client.user)
|
|
|
|
|
|
|
|
if (userAudiobook) {
|
|
|
|
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
|
|
|
id: userAudiobook.audiobookId,
|
|
|
|
data: userAudiobook.toJSON()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-20 01:45:56 +01:00
|
|
|
streamSyncFromApi(req, res) {
|
|
|
|
var user = req.user
|
|
|
|
var syncData = req.body
|
|
|
|
|
|
|
|
var stream = this.streams.find(s => s.id === syncData.streamId)
|
|
|
|
if (!stream) {
|
|
|
|
Logger.error(`[StreamManager] streamSyncFromApi stream not found ${syncData.streamId}`)
|
|
|
|
return res.status(404).send('Stream not found')
|
|
|
|
}
|
|
|
|
if (stream.userToken !== user.token) {
|
|
|
|
Logger.error(`[StreamManager] streamSyncFromApi Invalid stream not owned by user`)
|
|
|
|
return res.status(500).send('Invalid stream auth')
|
|
|
|
}
|
|
|
|
|
|
|
|
var listeningSession = stream.syncStream(syncData)
|
|
|
|
|
|
|
|
if (listeningSession && listeningSession.timeListening > 0) {
|
|
|
|
// Save listening session
|
|
|
|
var existingListeningSession = this.db.sessions.find(s => s.id === listeningSession.id)
|
|
|
|
if (existingListeningSession) {
|
|
|
|
this.db.updateEntity('session', listeningSession)
|
|
|
|
} else {
|
|
|
|
this.db.sessions.push(listeningSession.toJSON()) // Insert right away to prevent duplicate session
|
|
|
|
this.db.insertEntity('session', listeningSession)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var userAudiobook = user.updateAudiobookProgressFromStream(stream)
|
|
|
|
this.db.updateEntity('user', user)
|
|
|
|
|
|
|
|
if (userAudiobook) {
|
|
|
|
this.clientEmitter(user.id, 'current_user_audiobook_update', {
|
|
|
|
id: userAudiobook.audiobookId,
|
|
|
|
data: userAudiobook.toJSON()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
res.sendStatus(200)
|
|
|
|
}
|
|
|
|
|
2021-08-18 00:01:11 +02:00
|
|
|
streamUpdate(socket, { currentTime, streamId }) {
|
|
|
|
var client = socket.sheepClient
|
|
|
|
if (!client || !client.stream) {
|
2021-09-05 21:30:33 +02:00
|
|
|
Logger.error('No stream for client', (client && client.user) ? client.user.id : 'No Client')
|
2021-08-18 00:01:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if (client.stream.id !== streamId) {
|
|
|
|
Logger.error('Stream id mismatch on stream update', streamId, client.stream.id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
client.stream.updateClientCurrentTime(currentTime)
|
2021-08-18 00:43:29 +02:00
|
|
|
if (!client.user) {
|
|
|
|
Logger.error('No User for client', client)
|
|
|
|
return
|
|
|
|
}
|
2021-09-06 21:13:01 +02:00
|
|
|
if (!client.user.updateAudiobookProgressFromStream) {
|
2021-08-18 13:50:24 +02:00
|
|
|
Logger.error('Invalid User for client', client)
|
|
|
|
return
|
|
|
|
}
|
2021-10-24 22:53:51 +02:00
|
|
|
var userAudiobook = client.user.updateAudiobookProgressFromStream(client.stream)
|
2021-08-18 13:50:24 +02:00
|
|
|
this.db.updateEntity('user', client.user)
|
2021-10-24 22:53:51 +02:00
|
|
|
|
|
|
|
if (userAudiobook) {
|
2021-10-26 04:14:54 +02:00
|
|
|
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
2021-10-24 22:53:51 +02:00
|
|
|
id: userAudiobook.audiobookId,
|
|
|
|
data: userAudiobook.toJSON()
|
|
|
|
})
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = StreamManager
|