Update:Cleanup socket usage & add func for emitting events to admin users

This commit is contained in:
advplyr 2022-11-24 16:35:26 -06:00
parent e2af33e136
commit 180293ebc1
7 changed files with 90 additions and 105 deletions

View File

@ -378,7 +378,7 @@ export default {
} }
}, },
streamReady() { streamReady() {
console.log(`[STREAM-CONTAINER] Stream Ready`) console.log(`[StreamContainer] Stream Ready`)
if (this.$refs.audioPlayer) { if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setStreamReady() this.$refs.audioPlayer.setStreamReady()
} else { } else {

View File

@ -342,6 +342,7 @@ export default {
this.$root.socket = this.socket this.$root.socket = this.socket
console.log('Socket initialized') console.log('Socket initialized')
// Pre-defined socket events
this.socket.on('connect', this.connect) this.socket.on('connect', this.connect)
this.socket.on('connect_error', this.connectError) this.socket.on('connect_error', this.connectError)
this.socket.on('disconnect', this.disconnect) this.socket.on('disconnect', this.disconnect)
@ -350,6 +351,7 @@ export default {
this.socket.io.on('reconnect_error', this.reconnectError) this.socket.io.on('reconnect_error', this.reconnectError)
this.socket.io.on('reconnect_failed', this.reconnectFailed) this.socket.io.on('reconnect_failed', this.reconnectFailed)
// Event received after authorizing socket
this.socket.on('init', this.init) this.socket.on('init', this.init)
// Stream Listeners // Stream Listeners

View File

@ -258,23 +258,6 @@ class Db {
}) })
} }
updateUserStream(userId, streamId) {
return this.usersDb.update((record) => record.id === userId, (user) => {
user.stream = streamId
return user
}).then((results) => {
Logger.debug(`[DB] Updated user ${results.updated}`)
this.users = this.users.map(u => {
if (u.id === userId) {
u.stream = streamId
}
return u
})
}).catch((error) => {
Logger.error(`[DB] Update user Failed ${error}`)
})
}
updateServerSettings() { updateServerSettings() {
global.ServerSettings = this.serverSettings.toJSON() global.ServerSettings = this.serverSettings.toJSON()
return this.updateEntity('settings', this.serverSettings) return this.updateEntity('settings', this.serverSettings)

View File

@ -100,7 +100,7 @@ class Server {
Logger.info('[Server] Init v' + version) Logger.info('[Server] Init v' + version)
await this.playbackSessionManager.removeOrphanStreams() await this.playbackSessionManager.removeOrphanStreams()
var previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version const previousVersion = await this.db.checkPreviousVersion() // Returns null if same server version
if (previousVersion) { if (previousVersion) {
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`) Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
} }
@ -167,13 +167,13 @@ class Server {
// EBook static file routes // EBook static file routes
router.get('/ebook/:library/:folder/*', (req, res) => { router.get('/ebook/:library/:folder/*', (req, res) => {
var library = this.db.libraries.find(lib => lib.id === req.params.library) const library = this.db.libraries.find(lib => lib.id === req.params.library)
if (!library) return res.sendStatus(404) if (!library) return res.sendStatus(404)
var folder = library.folders.find(fol => fol.id === req.params.folder) const folder = library.folders.find(fol => fol.id === req.params.folder)
if (!folder) return res.status(404).send('Folder not found') if (!folder) return res.status(404).send('Folder not found')
var remainingPath = req.params['0'] const remainingPath = req.params['0']
var fullPath = Path.join(folder.fullPath, remainingPath) const fullPath = Path.join(folder.fullPath, remainingPath)
res.sendFile(fullPath) res.sendFile(fullPath)
}) })
@ -264,15 +264,15 @@ class Server {
// Remove unused /metadata/items/{id} folders // Remove unused /metadata/items/{id} folders
async purgeMetadata() { async purgeMetadata() {
var itemsMetadata = Path.join(global.MetadataPath, 'items') const itemsMetadata = Path.join(global.MetadataPath, 'items')
if (!(await fs.pathExists(itemsMetadata))) return if (!(await fs.pathExists(itemsMetadata))) return
var foldersInItemsMetadata = await fs.readdir(itemsMetadata) const foldersInItemsMetadata = await fs.readdir(itemsMetadata)
var purged = 0 let purged = 0
await Promise.all(foldersInItemsMetadata.map(async foldername => { await Promise.all(foldersInItemsMetadata.map(async foldername => {
var hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername) const hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
if (!hasMatchingItem) { if (!hasMatchingItem) {
var folderPath = Path.join(itemsMetadata, foldername) const folderPath = Path.join(itemsMetadata, foldername)
Logger.debug(`[Server] Purging unused metadata ${folderPath}`) Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
await fs.remove(folderPath).then(() => { await fs.remove(folderPath).then(() => {
@ -291,8 +291,8 @@ class Server {
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist // Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
async cleanUserData() { async cleanUserData() {
for (let i = 0; i < this.db.users.length; i++) { for (let i = 0; i < this.db.users.length; i++) {
var _user = this.db.users[i] const _user = this.db.users[i]
var hasUpdated = false let hasUpdated = false
if (_user.mediaProgress.length) { if (_user.mediaProgress.length) {
const lengthBefore = _user.mediaProgress.length const lengthBefore = _user.mediaProgress.length
_user.mediaProgress = _user.mediaProgress.filter(mp => { _user.mediaProgress = _user.mediaProgress.filter(mp => {
@ -338,9 +338,10 @@ class Server {
} }
logout(req, res) { logout(req, res) {
var { socketId } = req.body if (req.body.socketId) {
Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${socketId}`) Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`)
SocketAuthority.logout(socketId) SocketAuthority.logout(req.body.socketId)
}
res.sendStatus(200) res.sendStatus(200)
} }

View File

@ -30,24 +30,37 @@ class SocketAuthority {
return Object.values(this.clients).filter(c => c.user && c.user.id === userId) return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
} }
// Emits event to all authorized clients
emitter(evt, data) { emitter(evt, data) {
for (const socketId in this.clients) { for (const socketId in this.clients) {
if (this.clients[socketId].user) {
this.clients[socketId].socket.emit(evt, data) this.clients[socketId].socket.emit(evt, data)
} }
} }
}
clientEmitter(userId, ev, data) { // Emits event to all clients for a specific user
var clients = this.getClientsForUser(userId) clientEmitter(userId, evt, data) {
const clients = this.getClientsForUser(userId)
if (!clients.length) { if (!clients.length) {
return Logger.debug(`[Server] clientEmitter - no clients found for user ${userId}`) return Logger.debug(`[Server] clientEmitter - no clients found for user ${userId}`)
} }
clients.forEach((client) => { clients.forEach((client) => {
if (client.socket) { if (client.socket) {
client.socket.emit(ev, data) client.socket.emit(evt, data)
} }
}) })
} }
// Emits event to all admin user clients
adminEmitter(evt, data) {
for (const socketId in this.clients) {
if (this.clients[socketId].user && this.clients[socketId].user.isAdminOrUp) {
this.clients[socketId].socket.emit(evt, data)
}
}
}
initialize(Server) { initialize(Server) {
this.Server = Server this.Server = Server
@ -78,7 +91,29 @@ class SocketAuthority {
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
socket.on('fetch_daily_logs', () => this.Server.logManager.socketRequestDailyLogs(socket)) socket.on('fetch_daily_logs', () => this.Server.logManager.socketRequestDailyLogs(socket))
// Sent automatically from socket.io clients
socket.on('disconnect', (reason) => {
Logger.removeSocketListener(socket.id)
const _client = this.clients[socket.id]
if (!_client) {
Logger.warn(`[Server] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)
} else if (!_client.user) {
Logger.info(`[Server] Unauth socket ${socket.id} disconnected (Reason: ${reason})`)
delete this.clients[socket.id]
} else {
Logger.debug('[Server] User Offline ' + _client.user.username)
this.adminEmitter('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
const disconnectTime = Date.now() - _client.connected_at
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
delete this.clients[socket.id]
}
})
//
// Events for testing // Events for testing
//
socket.on('message_all_users', (payload) => { socket.on('message_all_users', (payload) => {
// admin user can send a message to all authenticated users // admin user can send a message to all authenticated users
// displays on the web app as a toast // displays on the web app as a toast
@ -95,26 +130,6 @@ class SocketAuthority {
Logger.debug(`[Server] Received ping from socket ${user.username || 'No User'}`) Logger.debug(`[Server] Received ping from socket ${user.username || 'No User'}`)
socket.emit('pong') socket.emit('pong')
}) })
// Sent automatically from socket.io clients
socket.on('disconnect', (reason) => {
Logger.removeSocketListener(socket.id)
const _client = this.clients[socket.id]
if (!_client) {
Logger.warn(`[Server] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)
} else if (!_client.user) {
Logger.info(`[Server] Unauth socket ${socket.id} disconnected (Reason: ${reason})`)
delete this.clients[socket.id]
} else {
Logger.debug('[Server] User Offline ' + _client.user.username)
this.io.emit('user_offline', _client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
const disconnectTime = Date.now() - _client.connected_at
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
delete this.clients[socket.id]
}
})
}) })
} }
@ -141,9 +156,9 @@ class SocketAuthority {
Logger.debug(`[Server] User Online ${client.user.username}`) Logger.debug(`[Server] User Online ${client.user.username}`)
// TODO: Send to authenticated clients only this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
this.io.emit('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
// Update user lastSeen
user.lastSeen = Date.now() user.lastSeen = Date.now()
await this.Server.db.updateEntity('user', user) await this.Server.db.updateEntity('user', user)
@ -155,20 +170,19 @@ class SocketAuthority {
if (user.isAdminOrUp) { if (user.isAdminOrUp) {
initialPayload.usersOnline = this.getUsersOnline() initialPayload.usersOnline = this.getUsersOnline()
} }
client.socket.emit('init', initialPayload) client.socket.emit('init', initialPayload)
} }
logout(socketId) { logout(socketId) {
// Strip user and client from client and client socket // Strip user and client from client and client socket
if (socketId && this.clients[socketId]) { if (socketId && this.clients[socketId]) {
var client = this.clients[socketId] const client = this.clients[socketId]
var clientSocket = client.socket const clientSocket = client.socket
Logger.debug(`[Server] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`) Logger.debug(`[Server] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`)
if (client.user) { if (client.user) {
Logger.debug('[Server] User Offline ' + client.user.username) Logger.debug('[Server] User Offline ' + client.user.username)
this.io.emit('user_offline', client.user.toJSONForPublic(null, this.Server.db.libraryItems)) this.adminEmitter('user_offline', client.user.toJSONForPublic(null, this.Server.db.libraryItems))
} }
delete this.clients[socketId].user delete this.clients[socketId].user

View File

@ -169,7 +169,7 @@ class PlaybackSessionManager {
user.currentSessionId = newPlaybackSession.id user.currentSessionId = newPlaybackSession.id
this.sessions.push(newPlaybackSession) this.sessions.push(newPlaybackSession)
SocketAuthority.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems)) SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
return newPlaybackSession return newPlaybackSession
} }
@ -213,7 +213,7 @@ class PlaybackSessionManager {
await this.saveSession(session) await this.saveSession(session)
} }
Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`) Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
SocketAuthority.emitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems)) SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions, this.db.libraryItems))
return this.removeSession(session.id) return this.removeSession(session.id)
} }

View File

@ -262,13 +262,13 @@ class ApiRouter {
async getDirectories(dir, relpath, excludedDirs, level = 0) { async getDirectories(dir, relpath, excludedDirs, level = 0) {
try { try {
var paths = await fs.readdir(dir) const paths = await fs.readdir(dir)
var dirs = await Promise.all(paths.map(async dirname => { let dirs = await Promise.all(paths.map(async dirname => {
var fullPath = Path.join(dir, dirname) const fullPath = Path.join(dir, dirname)
var path = Path.join(relpath, dirname) const path = Path.join(relpath, dirname)
var isDir = (await fs.lstat(fullPath)).isDirectory() const isDir = (await fs.lstat(fullPath)).isDirectory()
if (isDir && !excludedDirs.includes(path) && dirname !== 'node_modules') { if (isDir && !excludedDirs.includes(path) && dirname !== 'node_modules') {
return { return {
path, path,
@ -293,13 +293,13 @@ class ApiRouter {
// Helper Methods // Helper Methods
// //
userJsonWithItemProgressDetails(user, hideRootToken = false) { userJsonWithItemProgressDetails(user, hideRootToken = false) {
var json = user.toJSONForBrowser() const json = user.toJSONForBrowser()
if (json.type === 'root' && hideRootToken) { if (json.type === 'root' && hideRootToken) {
json.token = '' json.token = ''
} }
json.mediaProgress = json.mediaProgress.map(lip => { json.mediaProgress = json.mediaProgress.map(lip => {
var libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId) const libraryItem = this.db.libraryItems.find(li => li.id === lip.libraryItemId)
if (!libraryItem) { if (!libraryItem) {
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId) Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
lip.media = null lip.media = null
@ -326,31 +326,18 @@ class ApiRouter {
async handleDeleteLibraryItem(libraryItem) { async handleDeleteLibraryItem(libraryItem) {
// Remove libraryItem from users // Remove libraryItem from users
for (let i = 0; i < this.db.users.length; i++) { for (let i = 0; i < this.db.users.length; i++) {
var user = this.db.users[i] const user = this.db.users[i]
var madeUpdates = user.removeMediaProgressForLibraryItem(libraryItem.id) if (user.removeMediaProgressForLibraryItem(libraryItem.id)) {
if (madeUpdates) {
await this.db.updateEntity('user', user) await this.db.updateEntity('user', user)
} }
} }
// remove any streams open for this audiobook // TODO: Remove open sessions for library item
// TODO: Change to PlaybackSessionManager to remove open sessions for user
// var streams = this.streamManager.streams.filter(stream => stream.audiobookId === libraryItem.id)
// for (let i = 0; i < streams.length; i++) {
// var stream = streams[i]
// var client = stream.client
// await stream.close()
// if (client && client.user) {
// client.user.stream = null
// client.stream = null
// this.db.updateUserStream(client.user.id, null)
// }
// }
// remove book from collections // remove book from collections
var collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id)) const collectionsWithBook = this.db.collections.filter(c => c.books.includes(libraryItem.id))
for (let i = 0; i < collectionsWithBook.length; i++) { for (let i = 0; i < collectionsWithBook.length; i++) {
var collection = collectionsWithBook[i] const collection = collectionsWithBook[i]
collection.removeBook(libraryItem.id) collection.removeBook(libraryItem.id)
await this.db.updateEntity('collection', collection) await this.db.updateEntity('collection', collection)
SocketAuthority.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.libraryItems)) SocketAuthority.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.libraryItems))
@ -361,34 +348,32 @@ class ApiRouter {
await this.cacheManager.purgeCoverCache(libraryItem.id) await this.cacheManager.purgeCoverCache(libraryItem.id)
} }
var json = libraryItem.toJSONExpanded()
await this.db.removeLibraryItem(libraryItem.id) await this.db.removeLibraryItem(libraryItem.id)
SocketAuthority.emitter('item_removed', json) SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
} }
async getUserListeningSessionsHelper(userId) { async getUserListeningSessionsHelper(userId) {
var userSessions = await this.db.selectUserSessions(userId) const userSessions = await this.db.selectUserSessions(userId)
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
} }
async getAllSessionsWithUserData() { async getAllSessionsWithUserData() {
var sessions = await this.db.getAllSessions() const sessions = await this.db.getAllSessions()
sessions.sort((a, b) => b.updatedAt - a.updatedAt) sessions.sort((a, b) => b.updatedAt - a.updatedAt)
return sessions.map(se => { return sessions.map(se => {
var user = this.db.users.find(u => u.id === se.userId) const user = this.db.users.find(u => u.id === se.userId)
var _se = { return {
...se, ...se,
user: user ? { id: user.id, username: user.username } : null user: user ? { id: user.id, username: user.username } : null
} }
return _se
}) })
} }
async getUserListeningStatsHelpers(userId) { async getUserListeningStatsHelpers(userId) {
const today = date.format(new Date(), 'YYYY-MM-DD') const today = date.format(new Date(), 'YYYY-MM-DD')
var listeningSessions = await this.getUserListeningSessionsHelper(userId) const listeningSessions = await this.getUserListeningSessionsHelper(userId)
var listeningStats = { const listeningStats = {
totalTime: 0, totalTime: 0,
items: {}, items: {},
days: {}, days: {},
@ -397,7 +382,7 @@ class ApiRouter {
recentSessions: listeningSessions.slice(0, 10) recentSessions: listeningSessions.slice(0, 10)
} }
listeningSessions.forEach((s) => { listeningSessions.forEach((s) => {
var sessionTimeListening = s.timeListening let sessionTimeListening = s.timeListening
if (typeof sessionTimeListening == 'string') { if (typeof sessionTimeListening == 'string') {
sessionTimeListening = Number(sessionTimeListening) sessionTimeListening = Number(sessionTimeListening)
} }
@ -432,15 +417,15 @@ class ApiRouter {
async createAuthorsAndSeriesForItemUpdate(mediaPayload) { async createAuthorsAndSeriesForItemUpdate(mediaPayload) {
if (mediaPayload.metadata) { if (mediaPayload.metadata) {
var mediaMetadata = mediaPayload.metadata const mediaMetadata = mediaPayload.metadata
// Create new authors if in payload // Create new authors if in payload
if (mediaMetadata.authors && mediaMetadata.authors.length) { if (mediaMetadata.authors && mediaMetadata.authors.length) {
// TODO: validate authors // TODO: validate authors
var newAuthors = [] const newAuthors = []
for (let i = 0; i < mediaMetadata.authors.length; i++) { for (let i = 0; i < mediaMetadata.authors.length; i++) {
if (mediaMetadata.authors[i].id.startsWith('new')) { if (mediaMetadata.authors[i].id.startsWith('new')) {
var author = this.db.authors.find(au => au.checkNameEquals(mediaMetadata.authors[i].name)) let author = this.db.authors.find(au => au.checkNameEquals(mediaMetadata.authors[i].name))
if (!author) { if (!author) {
author = new Author() author = new Author()
author.setData(mediaMetadata.authors[i]) author.setData(mediaMetadata.authors[i])
@ -461,10 +446,10 @@ class ApiRouter {
// Create new series if in payload // Create new series if in payload
if (mediaMetadata.series && mediaMetadata.series.length) { if (mediaMetadata.series && mediaMetadata.series.length) {
// TODO: validate series // TODO: validate series
var newSeries = [] const newSeries = []
for (let i = 0; i < mediaMetadata.series.length; i++) { for (let i = 0; i < mediaMetadata.series.length; i++) {
if (mediaMetadata.series[i].id.startsWith('new')) { if (mediaMetadata.series[i].id.startsWith('new')) {
var seriesItem = this.db.series.find(se => se.checkNameEquals(mediaMetadata.series[i].name)) let seriesItem = this.db.series.find(se => se.checkNameEquals(mediaMetadata.series[i].name))
if (!seriesItem) { if (!seriesItem) {
seriesItem = new Series() seriesItem = new Series()
seriesItem.setData(mediaMetadata.series[i]) seriesItem.setData(mediaMetadata.series[i])