diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index e277966e..9777f28e 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -378,7 +378,7 @@ export default { } }, streamReady() { - console.log(`[STREAM-CONTAINER] Stream Ready`) + console.log(`[StreamContainer] Stream Ready`) if (this.$refs.audioPlayer) { this.$refs.audioPlayer.setStreamReady() } else { diff --git a/client/components/tables/BackupsTable.vue b/client/components/tables/BackupsTable.vue index eb16dadb..98f017ad 100644 --- a/client/components/tables/BackupsTable.vue +++ b/client/components/tables/BackupsTable.vue @@ -64,13 +64,11 @@ export default { showConfirmApply: false, selectedBackup: null, isBackingUp: false, - processing: false + processing: false, + backups: [] } }, computed: { - backups() { - return this.$store.state.backups || [] - }, userToken() { return this.$store.getters['user/getToken'] } @@ -96,9 +94,8 @@ export default { this.processing = true this.$axios .$delete(`/api/backups/${backup.id}`) - .then((backups) => { - console.log('Backup deleted', backups) - this.$store.commit('setBackups', backups) + .then((data) => { + this.setBackups(data.backups || []) this.$toast.success(this.$strings.ToastBackupDeleteSuccess) this.processing = false }) @@ -117,10 +114,10 @@ export default { this.isBackingUp = true this.$axios .$post('/api/backups') - .then((backups) => { + .then((data) => { this.isBackingUp = false this.$toast.success(this.$strings.ToastBackupCreateSuccess) - this.$store.commit('setBackups', backups) + this.setBackups(data.backups || []) }) .catch((error) => { this.isBackingUp = false @@ -136,9 +133,8 @@ export default { this.$axios .$post('/api/backups/upload', form) - .then((result) => { - console.log('Upload backup result', result) - this.$store.commit('setBackups', result) + .then((data) => { + this.setBackups(data.backups || []) this.$toast.success(this.$strings.ToastBackupUploadSuccess) this.processing = false }) @@ -148,9 +144,29 @@ export default { this.$toast.error(errorMessage) this.processing = false }) + }, + setBackups(backups) { + backups.sort((a, b) => b.createdAt - a.createdAt) + this.backups = backups + }, + loadBackups() { + this.processing = true + this.$axios + .$get('/api/backups') + .then((data) => { + this.setBackups(data.backups || []) + }) + .catch((error) => { + console.error('Failed to load backups', error) + this.$toast.error('Failed to load backups') + }) + .finally(() => { + this.processing = false + }) } }, mounted() { + this.loadBackups() if (this.$route.query.backup) { this.$toast.success('Backup applied successfully') this.$router.replace('/config') diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index c4459b0d..8fb644ea 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -26,11 +26,9 @@
Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}
-Last: {{ user.mostRecent.metadata.title }}
+Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}
+Last: {{ usersOnline[user.id].mostRecent.media.metadata.title }}
http://192.168.1.1:8337
then you would put http://192.168.1.1:8337/notify
.",
+ "MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji Apprise API albo innego rozwiązania, które obsługuje schemat zapytań Apprise. http://192.168.1.1:8337
to wpisany tutaj URL powinien mieć postać: http://192.168.1.1:8337/notify
.",
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w",
"MessageBackupsNote": "Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
"MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.",
- "MessageBookshelfNoCollections": "You haven't made any collections yet",
- "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
- "MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
- "MessageBookshelfNoSeries": "You have no series",
+ "MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji",
+ "MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"",
+ "MessageBookshelfNoRSSFeeds": "Nie posiadasz żadnych otwartych feedów RSS",
+ "MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
"MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka",
"MessageCheckingCron": "Sprawdzanie cron...",
@@ -456,7 +456,7 @@
"MessageNoEpisodes": "Brak odcinków",
"MessageNoFoldersAvailable": "Brak dostępnych folderów",
"MessageNoGenres": "Brak gatunków",
- "MessageNoIssues": "No Issues",
+ "MessageNoIssues": "Brak problemów",
"MessageNoItems": "Brak elementów",
"MessageNoItemsFound": "Nie znaleziono żadnych elemntów",
"MessageNoListeningSessions": "Brak sesji odtwarzania",
@@ -466,16 +466,16 @@
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
"MessageNoResults": "Brak wyników",
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
- "MessageNotYetImplemented": "Not yet implemented",
+ "MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
"MessageNoUpdateNecessary": "Brak konieczności aktualizacji",
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
- "MessageOr": "or",
- "MessagePauseChapter": "Pause chapter playback",
- "MessagePlayChapter": "Listen to beginning of chapter",
+ "MessageOr": "lub",
+ "MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
+ "MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
"MessageRemoveAllItemsWarning": "UWAGA! Ta akcja usunie wszystkie elementy biblioteki z bazy danych, w tym wszystkie aktualizacje lub dopasowania, które zostały wykonane. Pliki pozostaną niezmienione. Czy jesteś pewien?",
- "MessageRemoveChapter": "Remove chapter",
+ "MessageRemoveChapter": "Usuń rozdział",
"MessageRemoveEpisodes": "Usuń {0} odcinków",
"MessageRemoveUserWarning": "Czy na pewno chcesz trwale usunąć użytkownika \"{0}\"?",
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
@@ -490,12 +490,12 @@
"MessageUploading": "Przesyłanie...",
"MessageValidCronExpression": "Sprawdź wyrażenie CRON",
"MessageWatcherIsDisabledGlobally": "Watcher jest wyłączony globalnie w ustawieniach serwera",
- "MessageXLibraryIsEmpty": "{0} Library is empty!",
+ "MessageXLibraryIsEmpty": "{0} Biblioteka jest pusta!",
"MessageYourAudiobookDurationIsLonger": "Czas trwania Twojego audiobooka jest dłuższy niż znaleziony czas trwania",
"MessageYourAudiobookDurationIsShorter": "Czas trwania Twojego audiobooka jest krótszy niż znaleziony czas trwania",
"NoteChangeRootPassword": "Tylko użytkownik root, może posiadać puste hasło",
"NoteChapterEditorTimes": "Uwaga: Czas rozpoczęcia pierwszego rozdziału musi pozostać na poziomie 0:00, a czas rozpoczęcia ostatniego rozdziału nie może przekroczyć czasu trwania audiobooka.",
- "NoteFolderPicker": "Note: folders already mapped will not be shown",
+ "NoteFolderPicker": "Uwaga: dotychczas zmapowane foldery nie zostaną wyświetlone",
"NoteFolderPickerDebian": "Uwaga: Wybór folderu w instalcji opartej o system debian nie jest w pełni zaimplementowany. Powinieneś wprowadzić ścieżkę do swojej biblioteki bezpośrednio.",
"NoteRSSFeedPodcastAppsHttps": "Ostrzeżenie: Większość aplikacji do obsługi podcastów wymaga, aby adres URL kanału RSS korzystał z protokołu HTTPS.",
"NoteRSSFeedPodcastAppsPubDate": "Ostrzeżenie: 1 lub więcej odcinków nie ma daty publikacji. Niektóre aplikacje do słuchania podcastów tego wymagają.",
@@ -515,8 +515,8 @@
"ToastAuthorUpdateSuccessNoImageFound": "Autor zaktualizowany (nie znaleziono obrazu)",
"ToastBackupCreateFailed": "Nie udało się utworzyć kopii zapasowej",
"ToastBackupCreateSuccess": "Utworzono kopię zapasową",
- "ToastBackupDeleteFailed": "Failed to delete backup",
- "ToastBackupDeleteSuccess": "Nie udało się usunąć kopii zapasowej",
+ "ToastBackupDeleteFailed": "Nie udało się usunąć kopii zapasowej",
+ "ToastBackupDeleteSuccess": "Udało się usunąć kopie zapasowej",
"ToastBackupRestoreFailed": "Nie udało się przywrócić kopii zapasowej",
"ToastBackupUploadFailed": "Nie udało się przesłać kopii zapasowej",
"ToastBackupUploadSuccess": "Kopia zapasowa została przesłana",
@@ -559,9 +559,9 @@
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
"ToastSessionDeleteSuccess": "Sesja usunięta",
- "ToastSocketConnected": "Socket connected",
- "ToastSocketDisconnected": "Socket disconnected",
- "ToastSocketFailedToConnect": "Socket failed to connect",
+ "ToastSocketConnected": "Nawiązano połączenie z serwerem",
+ "ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
+ "ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
"ToastUserDeleteSuccess": "Użytkownik usunięty",
"WeekdayFriday": "Piątek",
diff --git a/server/Db.js b/server/Db.js
index 040d3c72..2ebbee2e 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -270,23 +270,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() {
global.ServerSettings = this.serverSettings.toJSON()
return this.updateEntity('settings', this.serverSettings)
diff --git a/server/Server.js b/server/Server.js
index 7983809f..ef343f17 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -1,7 +1,6 @@
const Path = require('path')
const express = require('express')
const http = require('http')
-const SocketIO = require('socket.io')
const fs = require('./libs/fsExtra')
const fileUpload = require('./libs/expressFileupload')
const rateLimit = require('./libs/expressRateLimit')
@@ -13,11 +12,11 @@ const dbMigration = require('./utils/dbMigration')
const filePerms = require('./utils/filePerms')
const Logger = require('./Logger')
-// Classes
const Auth = require('./Auth')
const Watcher = require('./Watcher')
const Scanner = require('./scanner/Scanner')
const Db = require('./Db')
+const SocketAuthority = require('./SocketAuthority')
const ApiRouter = require('./routers/ApiRouter')
const HlsRouter = require('./routers/HlsRouter')
@@ -67,59 +66,30 @@ class Server {
this.auth = new Auth(this.db)
// Managers
- this.taskManager = new TaskManager(this.emitter.bind(this))
- this.notificationManager = new NotificationManager(this.db, this.emitter.bind(this))
- this.backupManager = new BackupManager(this.db, this.emitter.bind(this))
+ this.taskManager = new TaskManager()
+ this.notificationManager = new NotificationManager(this.db)
+ this.backupManager = new BackupManager(this.db)
this.logManager = new LogManager(this.db)
this.cacheManager = new CacheManager()
- this.abMergeManager = new AbMergeManager(this.db, this.taskManager, this.clientEmitter.bind(this))
- this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
+ this.abMergeManager = new AbMergeManager(this.db, this.taskManager)
+ this.playbackSessionManager = new PlaybackSessionManager(this.db)
this.coverManager = new CoverManager(this.db, this.cacheManager)
- this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this), this.notificationManager)
- this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager, this.emitter.bind(this), this.clientEmitter.bind(this))
- this.rssFeedManager = new RssFeedManager(this.db, this.emitter.bind(this))
+ this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
+ this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
+ this.rssFeedManager = new RssFeedManager(this.db)
- this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
+ this.scanner = new Scanner(this.db, this.coverManager)
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
// Routers
- this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.rssFeedManager, this.cronManager, this.notificationManager, this.taskManager, this.getUsersOnline.bind(this), this.emitter.bind(this), this.clientEmitter.bind(this))
- this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
+ this.apiRouter = new ApiRouter(this)
+ this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
this.staticRouter = new StaticRouter(this.db)
Logger.logManager = this.logManager
this.server = null
this.io = null
-
- this.clients = {}
- }
-
- getUsersOnline() {
- return Object.values(this.clients).filter(c => c.user).map(client => {
- return client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems)
- })
- }
-
- getClientsForUser(userId) {
- return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
- }
-
- emitter(ev, data) {
- // Logger.debug('EMITTER', ev)
- this.io.emit(ev, data)
- }
-
- clientEmitter(userId, ev, data) {
- var clients = this.getClientsForUser(userId)
- if (!clients.length) {
- return Logger.debug(`[Server] clientEmitter - no clients found for user ${userId}`)
- }
- clients.forEach((client) => {
- if (client.socket) {
- client.socket.emit(ev, data)
- }
- })
}
authMiddleware(req, res, next) {
@@ -130,7 +100,7 @@ class Server {
Logger.info('[Server] Init v' + version)
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) {
Logger.debug(`[Server] Upgraded from previous version ${previousVersion}`)
}
@@ -197,13 +167,13 @@ class Server {
// EBook static file routes
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)
- 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')
- var remainingPath = req.params['0']
- var fullPath = Path.join(folder.fullPath, remainingPath)
+ const remainingPath = req.params['0']
+ const fullPath = Path.join(folder.fullPath, remainingPath)
res.sendFile(fullPath)
})
@@ -272,58 +242,8 @@ class Server {
Logger.info(`Listening on http://${this.Host}:${this.Port}`)
})
- this.io = new SocketIO.Server(this.server, {
- cors: {
- origin: '*',
- methods: ["GET", "POST"]
- }
- })
- this.io.on('connection', (socket) => {
- this.clients[socket.id] = {
- id: socket.id,
- socket,
- connected_at: Date.now()
- }
- socket.sheepClient = this.clients[socket.id]
-
- Logger.info('[Server] Socket Connected', socket.id)
-
- socket.on('auth', (token) => this.authenticateSocket(socket, token))
-
- // Scanning
- socket.on('cancel_scan', this.cancelScan.bind(this))
-
- // Logs
- socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
- socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
- socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket))
-
- socket.on('ping', () => {
- var client = this.clients[socket.id] || {}
- var user = client.user || {}
- Logger.debug(`[Server] Received ping from socket ${user.username || 'No User'}`)
- socket.emit('pong')
- })
-
- socket.on('disconnect', (reason) => {
- Logger.removeSocketListener(socket.id)
-
- var _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.playbackSessionManager.sessions, this.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]
- }
- })
- })
+ // Start listening for socket connections
+ SocketAuthority.initialize(this)
}
async initializeServer(req, res) {
@@ -342,22 +262,17 @@ class Server {
await this.scanner.scanFilesChanged(fileUpdates)
}
- cancelScan(id) {
- Logger.debug('[Server] Cancel scan', id)
- this.scanner.setCancelLibraryScan(id)
- }
-
// Remove unused /metadata/items/{id} folders
async purgeMetadata() {
- var itemsMetadata = Path.join(global.MetadataPath, 'items')
+ const itemsMetadata = Path.join(global.MetadataPath, 'items')
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 => {
- var hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
+ const hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
if (!hasMatchingItem) {
- var folderPath = Path.join(itemsMetadata, foldername)
+ const folderPath = Path.join(itemsMetadata, foldername)
Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
await fs.remove(folderPath).then(() => {
@@ -376,8 +291,8 @@ class Server {
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
async cleanUserData() {
for (let i = 0; i < this.db.users.length; i++) {
- var _user = this.db.users[i]
- var hasUpdated = false
+ const _user = this.db.users[i]
+ let hasUpdated = false
if (_user.mediaProgress.length) {
const lengthBefore = _user.mediaProgress.length
_user.mediaProgress = _user.mediaProgress.filter(mp => {
@@ -423,68 +338,14 @@ class Server {
}
logout(req, res) {
- var { socketId } = req.body
- Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${socketId}`)
-
- // Strip user and client from client and client socket
- if (socketId && this.clients[socketId]) {
- var client = this.clients[socketId]
- var clientSocket = client.socket
- Logger.debug(`[Server] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`)
-
- if (client.user) {
- Logger.debug('[Server] User Offline ' + client.user.username)
- this.io.emit('user_offline', client.user.toJSONForPublic(null, this.db.libraryItems))
- }
-
- delete this.clients[socketId].user
- if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
- } else if (socketId) {
- Logger.warn(`[Server] No client for socket ${socketId}`)
+ if (req.body.socketId) {
+ Logger.info(`[Server] User ${req.user ? req.user.username : 'Unknown'} is logging out with socket ${req.body.socketId}`)
+ SocketAuthority.logout(req.body.socketId)
}
res.sendStatus(200)
}
- async authenticateSocket(socket, token) {
- var user = await this.auth.authenticateUser(token)
- if (!user) {
- Logger.error('Cannot validate socket - invalid token')
- return socket.emit('invalid_token')
- }
- var client = this.clients[socket.id]
-
- if (client.user !== undefined) {
- Logger.debug(`[Server] Authenticating socket client already has user`, client.user.username)
- }
-
- client.user = user
-
- if (!client.user.toJSONForBrowser) {
- Logger.error('Invalid user...', client.user)
- return
- }
-
- Logger.debug(`[Server] User Online ${client.user.username}`)
-
- this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
-
- user.lastSeen = Date.now()
- await this.db.updateEntity('user', user)
-
- const initialPayload = {
- metadataPath: global.MetadataPath,
- configPath: global.ConfigPath,
- user: client.user.toJSONForBrowser(),
- librariesScanning: this.scanner.librariesScanning,
- backups: (this.backupManager.backups || []).map(b => b.toJSON())
- }
- if (user.type === 'root') {
- initialPayload.usersOnline = this.getUsersOnline()
- }
- client.socket.emit('init', initialPayload)
- }
-
async stop() {
await this.watcher.close()
Logger.info('Watcher Closed')
diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js
new file mode 100644
index 00000000..5488cf74
--- /dev/null
+++ b/server/SocketAuthority.js
@@ -0,0 +1,200 @@
+const SocketIO = require('socket.io')
+const Logger = require('./Logger')
+
+class SocketAuthority {
+ constructor() {
+ this.Server = null
+ this.io = null
+
+ this.clients = {}
+ }
+
+ // returns an array of User.toJSONForPublic with `connections` for the # of socket connections
+ // a user can have many socket connections
+ getUsersOnline() {
+ const onlineUsersMap = {}
+ Object.values(this.clients).filter(c => c.user).forEach(client => {
+ if (onlineUsersMap[client.user.id]) {
+ onlineUsersMap[client.user.id].connections++
+ } else {
+ onlineUsersMap[client.user.id] = {
+ ...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems),
+ connections: 1
+ }
+ }
+ })
+ return Object.values(onlineUsersMap)
+ }
+
+ getClientsForUser(userId) {
+ return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
+ }
+
+ // Emits event to all authorized clients
+ emitter(evt, data) {
+ for (const socketId in this.clients) {
+ if (this.clients[socketId].user) {
+ this.clients[socketId].socket.emit(evt, data)
+ }
+ }
+ }
+
+ // Emits event to all clients for a specific user
+ clientEmitter(userId, evt, data) {
+ const clients = this.getClientsForUser(userId)
+ if (!clients.length) {
+ return Logger.debug(`[Server] clientEmitter - no clients found for user ${userId}`)
+ }
+ clients.forEach((client) => {
+ if (client.socket) {
+ 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) {
+ this.Server = Server
+
+ this.io = new SocketIO.Server(this.Server.server, {
+ cors: {
+ origin: '*',
+ methods: ["GET", "POST"]
+ }
+ })
+ this.io.on('connection', (socket) => {
+ this.clients[socket.id] = {
+ id: socket.id,
+ socket,
+ connected_at: Date.now()
+ }
+ socket.sheepClient = this.clients[socket.id]
+
+ Logger.info('[Server] Socket Connected', socket.id)
+
+ // Required for associating a User with a socket
+ socket.on('auth', (token) => this.authenticateSocket(socket, token))
+
+ // Scanning
+ socket.on('cancel_scan', this.cancelScan.bind(this))
+
+ // Logs
+ socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
+ socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
+ 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
+ //
+ socket.on('message_all_users', (payload) => {
+ // admin user can send a message to all authenticated users
+ // displays on the web app as a toast
+ const client = this.clients[socket.id] || {}
+ if (client.user && client.user.isAdminOrUp) {
+ this.emitter('admin_message', payload.message || '')
+ } else {
+ Logger.error(`[Server] Non-admin user sent the message_all_users event`)
+ }
+ })
+ socket.on('ping', () => {
+ const client = this.clients[socket.id] || {}
+ const user = client.user || {}
+ Logger.debug(`[Server] Received ping from socket ${user.username || 'No User'}`)
+ socket.emit('pong')
+ })
+ })
+ }
+
+ // When setting up a socket connection the user needs to be associated with a socket id
+ // for this the client will send a 'auth' event that includes the users API token
+ async authenticateSocket(socket, token) {
+ const user = await this.Server.auth.authenticateUser(token)
+ if (!user) {
+ Logger.error('Cannot validate socket - invalid token')
+ return socket.emit('invalid_token')
+ }
+ const client = this.clients[socket.id]
+
+ if (client.user !== undefined) {
+ Logger.debug(`[Server] Authenticating socket client already has user`, client.user.username)
+ }
+
+ client.user = user
+
+ if (!client.user.toJSONForBrowser) {
+ Logger.error('Invalid user...', client.user)
+ return
+ }
+
+ Logger.debug(`[Server] User Online ${client.user.username}`)
+
+ this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions, this.Server.db.libraryItems))
+
+ // Update user lastSeen
+ user.lastSeen = Date.now()
+ await this.Server.db.updateEntity('user', user)
+
+ const initialPayload = {
+ userId: client.user.id,
+ username: client.user.username,
+ librariesScanning: this.Server.scanner.librariesScanning
+ }
+ if (user.isAdminOrUp) {
+ initialPayload.usersOnline = this.getUsersOnline()
+ }
+ client.socket.emit('init', initialPayload)
+ }
+
+ logout(socketId) {
+ // Strip user and client from client and client socket
+ if (socketId && this.clients[socketId]) {
+ const client = this.clients[socketId]
+ const clientSocket = client.socket
+ Logger.debug(`[Server] Found user client ${clientSocket.id}, Has user: ${!!client.user}, Socket has client: ${!!clientSocket.sheepClient}`)
+
+ if (client.user) {
+ Logger.debug('[Server] User Offline ' + client.user.username)
+ this.adminEmitter('user_offline', client.user.toJSONForPublic(null, this.Server.db.libraryItems))
+ }
+
+ delete this.clients[socketId].user
+ if (clientSocket && clientSocket.sheepClient) delete this.clients[socketId].socket.sheepClient
+ } else if (socketId) {
+ Logger.warn(`[Server] No client for socket ${socketId}`)
+ }
+ }
+
+ cancelScan(id) {
+ Logger.debug('[Server] Cancel scan', id)
+ this.Server.scanner.setCancelLibraryScan(id)
+ }
+}
+module.exports = new SocketAuthority()
\ No newline at end of file
diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js
index 318538b9..96daefe8 100644
--- a/server/controllers/AuthorController.js
+++ b/server/controllers/AuthorController.js
@@ -1,4 +1,6 @@
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const { reqSupportsWebp } = require('../utils/index')
const { createNewSortInstance } = require('../libs/fastSort')
@@ -93,18 +95,18 @@ class AuthorController {
})
if (itemsWithAuthor.length) {
await this.db.updateLibraryItems(itemsWithAuthor)
- this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
}
// Remove old author
await this.db.removeEntity('author', req.author.id)
- this.emitter('author_removed', req.author.toJSON())
+ SocketAuthority.emitter('author_removed', req.author.toJSON())
// Send updated num books for merged author
var numBooks = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
}).length
- this.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
res.json({
author: existingAuthor.toJSON(),
@@ -121,7 +123,7 @@ class AuthorController {
})
if (itemsWithAuthor.length) {
await this.db.updateLibraryItems(itemsWithAuthor)
- this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
}
}
@@ -129,7 +131,7 @@ class AuthorController {
var numBooks = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
}).length
- this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
res.json({
@@ -190,7 +192,7 @@ class AuthorController {
var numBooks = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
}).length
- this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
+ SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
res.json({
diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js
index 79b07fd1..7ee482a9 100644
--- a/server/controllers/BackupController.js
+++ b/server/controllers/BackupController.js
@@ -3,32 +3,29 @@ const Logger = require('../Logger')
class BackupController {
constructor() { }
- async create(req, res) {
- if (!req.user.isAdminOrUp) {
- Logger.error(`[BackupController] Non-admin user attempting to craete backup`, req.user)
- return res.sendStatus(403)
- }
+ getAll(req, res) {
+ res.json({
+ backups: this.backupManager.backups.map(b => b.toJSON())
+ })
+ }
+
+ create(req, res) {
this.backupManager.requestCreateBackup(res)
}
async delete(req, res) {
- if (!req.user.isAdminOrUp) {
- Logger.error(`[BackupController] Non-admin user attempting to delete backup`, req.user)
- return res.sendStatus(403)
- }
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
if (!backup) {
return res.sendStatus(404)
}
await this.backupManager.removeBackup(backup)
- res.json(this.backupManager.backups.map(b => b.toJSON()))
+
+ res.json({
+ backups: this.backupManager.backups.map(b => b.toJSON())
+ })
}
async upload(req, res) {
- if (!req.user.isAdminOrUp) {
- Logger.error(`[BackupController] Non-admin user attempting to upload backup`, req.user)
- return res.sendStatus(403)
- }
if (!req.files.file) {
Logger.error('[BackupController] Upload backup invalid')
return res.sendStatus(500)
@@ -37,10 +34,6 @@ class BackupController {
}
async apply(req, res) {
- if (!req.user.isAdminOrUp) {
- Logger.error(`[BackupController] Non-admin user attempting to apply backup`, req.user)
- return res.sendStatus(403)
- }
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
if (!backup) {
return res.sendStatus(404)
@@ -48,5 +41,13 @@ class BackupController {
await this.backupManager.requestApplyBackup(backup)
res.sendStatus(200)
}
+
+ middleware(req, res, next) {
+ if (!req.user.isAdminOrUp) {
+ Logger.error(`[BackupController] Non-admin user attempting to access backups`, req.user)
+ return res.sendStatus(403)
+ }
+ next()
+ }
}
module.exports = new BackupController()
\ No newline at end of file
diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js
index 8508eb73..9234abee 100644
--- a/server/controllers/CollectionController.js
+++ b/server/controllers/CollectionController.js
@@ -1,4 +1,6 @@
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const Collection = require('../objects/Collection')
class CollectionController {
@@ -13,7 +15,7 @@ class CollectionController {
}
var jsonExpanded = newCollection.toJSONExpanded(this.db.libraryItems)
await this.db.insertEntity('collection', newCollection)
- this.emitter('collection_added', jsonExpanded)
+ SocketAuthority.emitter('collection_added', jsonExpanded)
res.json(jsonExpanded)
}
@@ -32,7 +34,7 @@ class CollectionController {
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
if (wasUpdated) {
await this.db.updateEntity('collection', collection)
- this.emitter('collection_updated', jsonExpanded)
+ SocketAuthority.emitter('collection_updated', jsonExpanded)
}
res.json(jsonExpanded)
}
@@ -41,7 +43,7 @@ class CollectionController {
const collection = req.collection
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
await this.db.removeEntity('collection', collection.id)
- this.emitter('collection_removed', jsonExpanded)
+ SocketAuthority.emitter('collection_removed', jsonExpanded)
res.sendStatus(200)
}
@@ -60,7 +62,7 @@ class CollectionController {
collection.addBook(req.body.id)
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
await this.db.updateEntity('collection', collection)
- this.emitter('collection_updated', jsonExpanded)
+ SocketAuthority.emitter('collection_updated', jsonExpanded)
res.json(jsonExpanded)
}
@@ -71,7 +73,7 @@ class CollectionController {
collection.removeBook(req.params.bookId)
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
await this.db.updateEntity('collection', collection)
- this.emitter('collection_updated', jsonExpanded)
+ SocketAuthority.emitter('collection_updated', jsonExpanded)
}
res.json(collection.toJSONExpanded(this.db.libraryItems))
}
@@ -92,7 +94,7 @@ class CollectionController {
}
if (hasUpdated) {
await this.db.updateEntity('collection', collection)
- this.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
+ SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
}
res.json(collection.toJSONExpanded(this.db.libraryItems))
}
@@ -113,7 +115,7 @@ class CollectionController {
}
if (hasUpdated) {
await this.db.updateEntity('collection', collection)
- this.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
+ SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(this.db.libraryItems))
}
res.json(collection.toJSONExpanded(this.db.libraryItems))
}
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index dbc29ba3..b724ed0c 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -2,6 +2,7 @@ const Path = require('path')
const fs = require('../libs/fsExtra')
const filePerms = require('../utils/filePerms')
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
const Library = require('../objects/Library')
const libraryHelpers = require('../utils/libraryHelpers')
const { sort, createNewSortInstance } = require('../libs/fastSort')
@@ -43,7 +44,7 @@ class LibraryController {
library.setData(newLibraryPayload)
await this.db.insertEntity('library', library)
// TODO: Only emit to users that have access
- this.emitter('library_added', library.toJSON())
+ SocketAuthority.emitter('library_added', library.toJSON())
// Add library watcher
this.watcher.addLibrary(library)
@@ -120,7 +121,7 @@ class LibraryController {
}
}
await this.db.updateEntity('library', library)
- this.emitter('library_updated', library.toJSON())
+ SocketAuthority.emitter('library_updated', library.toJSON())
}
return res.json(library.toJSON())
}
@@ -147,7 +148,7 @@ class LibraryController {
var libraryJson = library.toJSON()
await this.db.removeEntity('library', library.id)
- this.emitter('library_removed', libraryJson)
+ SocketAuthority.emitter('library_removed', libraryJson)
return res.json(libraryJson)
}
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index 20df9dad..8f573312 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -1,4 +1,6 @@
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
const { ScanResult } = require('../utils/constants')
@@ -53,7 +55,7 @@ class LibraryItemController {
if (hasUpdates) {
Logger.debug(`[LibraryItemController] Updated now saving`)
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
}
res.json(libraryItem.toJSON())
}
@@ -97,7 +99,7 @@ class LibraryItemController {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
}
res.json({
updated: hasUpdates,
@@ -132,7 +134,7 @@ class LibraryItemController {
}
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
res.json({
success: true,
cover: result.cover
@@ -152,7 +154,7 @@ class LibraryItemController {
}
if (validationResult.updated) {
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
}
res.json({
success: true,
@@ -168,7 +170,7 @@ class LibraryItemController {
libraryItem.updateMediaCover('')
await this.cacheManager.purgeCoverCache(libraryItem.id)
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
}
res.sendStatus(200)
@@ -228,7 +230,7 @@ class LibraryItemController {
}
libraryItem.media.updateAudioTracks(orderedFileData)
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSON())
}
@@ -284,7 +286,7 @@ class LibraryItemController {
if (hasUpdates) {
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
itemsUpdated++
}
}
@@ -342,7 +344,7 @@ class LibraryItemController {
updates: itemsUpdated,
unmatched: itemsUnmatched
}
- this.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
+ SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
}
// DELETE: api/items/all
@@ -410,7 +412,7 @@ class LibraryItemController {
const wasUpdated = req.libraryItem.media.updateChapters(chapters)
if (wasUpdated) {
await this.db.updateLibraryItem(req.libraryItem)
- this.emitter('item_updated', req.libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
}
res.json({
diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js
index 8a4848ee..3029a170 100644
--- a/server/controllers/MeController.js
+++ b/server/controllers/MeController.js
@@ -1,4 +1,5 @@
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
const { sort } = require('../libs/fastSort')
const { isObject, toNumber } = require('../utils/index')
@@ -48,7 +49,7 @@ class MeController {
return res.sendStatus(200)
}
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.sendStatus(200)
}
@@ -62,7 +63,7 @@ class MeController {
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body)
if (wasUpdated) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.sendStatus(200)
}
@@ -82,7 +83,7 @@ class MeController {
var wasUpdated = req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)
if (wasUpdated) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.sendStatus(200)
}
@@ -91,7 +92,7 @@ class MeController {
async batchUpdateMediaProgress(req, res) {
var itemProgressPayloads = req.body
if (!itemProgressPayloads || !itemProgressPayloads.length) {
- return res.sendStatus(500)
+ return res.status(400).send('Missing request payload')
}
var shouldUpdate = false
@@ -107,7 +108,7 @@ class MeController {
if (shouldUpdate) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.sendStatus(200)
@@ -120,7 +121,7 @@ class MeController {
const { time, title } = req.body
var bookmark = req.user.createBookmark(libraryItem.id, time, title)
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.json(bookmark)
}
@@ -136,7 +137,7 @@ class MeController {
var bookmark = req.user.updateBookmark(libraryItem.id, time, title)
if (!bookmark) return res.sendStatus(500)
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.json(bookmark)
}
@@ -153,7 +154,7 @@ class MeController {
}
req.user.removeBookmark(libraryItem.id, time)
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.sendStatus(200)
}
@@ -233,7 +234,7 @@ class MeController {
Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`)
if (numServerProgressUpdates > 0) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.json({
@@ -288,7 +289,7 @@ class MeController {
const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id)
if (hasUpdated) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.json(req.user.toJSONForBrowser())
}
@@ -304,7 +305,7 @@ class MeController {
const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id)
if (hasUpdated) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.json(req.user.toJSONForBrowser())
}
@@ -314,7 +315,7 @@ class MeController {
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
if (hasUpdated) {
await this.db.updateEntity('user', req.user)
- this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.json(req.user.toJSONForBrowser())
}
diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js
index 5c0b5e1e..2c5c7b48 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -1,11 +1,14 @@
-const axios = require('axios')
-const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
+const fs = require('../libs/fsExtra')
+
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
-const LibraryItem = require('../objects/LibraryItem')
const { getFileTimestampsWithIno } = require('../utils/fileUtils')
const filePerms = require('../utils/filePerms')
+const LibraryItem = require('../objects/LibraryItem')
+
class PodcastController {
async create(req, res) {
@@ -75,7 +78,7 @@ class PodcastController {
}
await this.db.insertLibraryItem(libraryItem)
- this.emitter('item_added', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSONExpanded())
@@ -194,7 +197,7 @@ class PodcastController {
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
if (wasUpdated) {
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
}
res.json(libraryItem.toJSONExpanded())
@@ -229,7 +232,7 @@ class PodcastController {
}
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSON())
}
diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js
index 9f11ac51..678f30e0 100644
--- a/server/controllers/SeriesController.js
+++ b/server/controllers/SeriesController.js
@@ -1,4 +1,5 @@
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
class SeriesController {
constructor() { }
@@ -38,7 +39,7 @@ class SeriesController {
const hasUpdated = req.series.update(req.body)
if (hasUpdated) {
await this.db.updateEntity('series', req.series)
- this.emitter('series_updated', req.series)
+ SocketAuthority.emitter('series_updated', req.series)
}
res.json(req.series)
}
diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js
index 2c2caf21..2d460a9c 100644
--- a/server/controllers/UserController.js
+++ b/server/controllers/UserController.js
@@ -1,4 +1,6 @@
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const User = require('../objects/user/User')
const { getId, toNumber } = require('../utils/index')
@@ -44,7 +46,7 @@ class UserController {
var newUser = new User(account)
var success = await this.db.insertEntity('user', newUser)
if (success) {
- this.clientEmitter(req.user.id, 'user_added', newUser)
+ SocketAuthority.clientEmitter(req.user.id, 'user_added', newUser)
res.json({
user: newUser.toJSONForBrowser()
})
@@ -85,7 +87,7 @@ class UserController {
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
}
await this.db.updateEntity('user', user)
- this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
}
res.json({
@@ -109,7 +111,7 @@ class UserController {
var userJson = user.toJSONForBrowser()
await this.db.removeEntity('user', user.id)
- this.clientEmitter(req.user.id, 'user_removed', userJson)
+ SocketAuthority.clientEmitter(req.user.id, 'user_removed', userJson)
res.json({
success: true
})
@@ -170,7 +172,7 @@ class UserController {
if (progressPurged) {
Logger.info(`[UserController] Purged ${progressPurged} media progress for user ${user.username}`)
await this.db.updateEntity('user', user)
- this.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
+ SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser())
}
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
@@ -181,10 +183,9 @@ class UserController {
if (!req.user.isAdminOrUp) {
return res.sendStatus(403)
}
- const usersOnline = this.getUsersOnline()
res.json({
- usersOnline,
+ usersOnline: SocketAuthority.getUsersOnline(),
openSessions: this.playbackSessionManager.sessions
})
}
diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js
index 6fbce262..d5c33970 100644
--- a/server/managers/AbMergeManager.js
+++ b/server/managers/AbMergeManager.js
@@ -10,10 +10,9 @@ const { writeConcatFile } = require('../utils/ffmpegHelpers')
const toneHelpers = require('../utils/toneHelpers')
class AbMergeManager {
- constructor(db, taskManager, clientEmitter) {
+ constructor(db, taskManager) {
this.db = db
this.taskManager = taskManager
- this.clientEmitter = clientEmitter
this.itemsCacheDir = Path.join(global.MetadataPath, 'cache/items')
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js
index 25a93583..d4d17211 100644
--- a/server/managers/AudioMetadataManager.js
+++ b/server/managers/AudioMetadataManager.js
@@ -1,18 +1,20 @@
const Path = require('path')
-const fs = require('../libs/fsExtra')
const workerThreads = require('worker_threads')
+
+const SocketAuthority = require('../SocketAuthority')
const Logger = require('../Logger')
+
+const fs = require('../libs/fsExtra')
+
const filePerms = require('../utils/filePerms')
const { secondsToTimestamp } = require('../utils/index')
const { writeMetadataFile } = require('../utils/ffmpegHelpers')
const toneHelpers = require('../utils/toneHelpers')
class AudioMetadataMangaer {
- constructor(db, taskManager, emitter, clientEmitter) {
+ constructor(db, taskManager) {
this.db = db
this.taskManager = taskManager
- this.emitter = emitter
- this.clientEmitter = clientEmitter
}
updateMetadataForItem(user, libraryItem, useTone, forceEmbedChapters) {
@@ -40,7 +42,7 @@ class AudioMetadataMangaer {
audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
}
- this.emitter('audio_metadata_started', itemAudioMetadataPayload)
+ SocketAuthority.emitter('audio_metadata_started', itemAudioMetadataPayload)
// Write chapters file
var toneJsonPath = null
@@ -67,7 +69,7 @@ class AudioMetadataMangaer {
itemAudioMetadataPayload.results = results
itemAudioMetadataPayload.elapsed = elapsed
itemAudioMetadataPayload.finishedAt = Date.now()
- this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
+ SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
}
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir) {
@@ -77,7 +79,7 @@ class AudioMetadataMangaer {
ino: audioFile.ino,
filename: audioFile.metadata.filename
}
- this.emitter('audiofile_metadata_started', resultPayload)
+ SocketAuthority.emitter('audiofile_metadata_started', resultPayload)
// Backup audio file
try {
@@ -98,7 +100,7 @@ class AudioMetadataMangaer {
Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
}
- this.emitter('audiofile_metadata_finished', resultPayload)
+ SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
return resultPayload
}
@@ -115,7 +117,7 @@ class AudioMetadataMangaer {
audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
}
- this.emitter('audio_metadata_started', itemAudioMetadataPayload)
+ SocketAuthority.emitter('audio_metadata_started', itemAudioMetadataPayload)
var downloadsPath = Path.join(global.MetadataPath, 'downloads')
var outputDir = Path.join(downloadsPath, libraryItem.id)
@@ -143,7 +145,7 @@ class AudioMetadataMangaer {
itemAudioMetadataPayload.results = results
itemAudioMetadataPayload.elapsed = elapsed
itemAudioMetadataPayload.finishedAt = Date.now()
- this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
+ SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
}
updateAudioFileMetadataWithFfmpeg(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
@@ -154,7 +156,7 @@ class AudioMetadataMangaer {
ino: audioFile.ino,
filename: audioFile.metadata.filename
}
- this.emitter('audiofile_metadata_started', resultPayload)
+ SocketAuthority.emitter('audiofile_metadata_started', resultPayload)
Logger.debug(`[AudioFileMetadataManager] Starting audio file metadata encode for "${audioFile.metadata.filename}"`)
@@ -229,19 +231,19 @@ class AudioMetadataMangaer {
Logger.debug(`[AudioFileMetadataManager] Audio file replaced successfully "${inputPath}"`)
resultPayload.success = true
- this.emitter('audiofile_metadata_finished', resultPayload)
+ SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
resolve(resultPayload)
}).catch((error) => {
Logger.error(`[AudioFileMetadataManager] Audio file failed to move "${inputPath}"`, error)
resultPayload.success = false
- this.emitter('audiofile_metadata_finished', resultPayload)
+ SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
resolve(resultPayload)
})
} else {
Logger.debug(`[AudioFileMetadataManager] Metadata encode FAILED for "${audioFile.metadata.filename}"`)
resultPayload.success = false
- this.emitter('audiofile_metadata_finished', resultPayload)
+ SocketAuthority.emitter('audiofile_metadata_finished', resultPayload)
resolve(resultPayload)
}
} else if (message.type === 'FFMPEG') {
diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js
index 01f05b90..4e387fa8 100644
--- a/server/managers/BackupManager.js
+++ b/server/managers/BackupManager.js
@@ -1,4 +1,6 @@
const Path = require('path')
+const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
const cron = require('../libs/nodeCron')
const fs = require('../libs/fsExtra')
@@ -8,18 +10,16 @@ const StreamZip = require('../libs/nodeStreamZip')
// Utils
const { getFileSize } = require('../utils/fileUtils')
const filePerms = require('../utils/filePerms')
-const Logger = require('../Logger')
const Backup = require('../objects/Backup')
class BackupManager {
- constructor(db, emitter) {
+ constructor(db) {
this.BackupPath = Path.join(global.MetadataPath, 'backups')
this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
this.AuthorsMetadataPath = Path.join(global.MetadataPath, 'authors')
this.db = db
- this.emitter = emitter
this.scheduleTask = null
@@ -106,13 +106,20 @@ class BackupManager {
this.backups.push(backup)
}
- return res.json(this.backups.map(b => b.toJSON()))
+ res.json({
+ backups: this.backups.map(b => b.toJSON())
+ })
}
async requestCreateBackup(res) {
var backupSuccess = await this.runBackup()
- if (backupSuccess) res.json(this.backups.map(b => b.toJSON()))
- else res.sendStatus(500)
+ if (backupSuccess) {
+ res.json({
+ backups: this.backups.map(b => b.toJSON())
+ })
+ } else {
+ res.sendStatus(500)
+ }
}
async requestApplyBackup(backup) {
@@ -123,7 +130,7 @@ class BackupManager {
await zip.extract('metadata-authors/', this.AuthorsMetadataPath)
}
await this.db.reinit()
- this.emitter('backup_applied')
+ SocketAuthority.emitter('backup_applied')
}
async loadBackups() {
diff --git a/server/managers/NotificationManager.js b/server/managers/NotificationManager.js
index cf7a6042..33d13ab5 100644
--- a/server/managers/NotificationManager.js
+++ b/server/managers/NotificationManager.js
@@ -1,11 +1,11 @@
const axios = require('axios')
const Logger = require("../Logger")
+const SocketAuthority = require('../SocketAuthority')
const { notificationData } = require('../utils/notifications')
class NotificationManager {
- constructor(db, emitter) {
+ constructor(db) {
this.db = db
- this.emitter = emitter
this.sendingNotification = false
this.notificationQueue = []
@@ -58,7 +58,7 @@ class NotificationManager {
}
await this.db.updateEntity('settings', this.db.notificationSettings)
- this.emitter('notifications_updated', this.db.notificationSettings)
+ SocketAuthority.emitter('notifications_updated', this.db.notificationSettings)
this.notificationFinished()
}
diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js
index 38693c97..23f991ad 100644
--- a/server/managers/PlaybackSessionManager.js
+++ b/server/managers/PlaybackSessionManager.js
@@ -1,22 +1,24 @@
const Path = require('path')
-const date = require('../libs/dateAndTime')
const serverVersion = require('../../package.json').version
-const { PlayMethod } = require('../utils/constants')
-const PlaybackSession = require('../objects/PlaybackSession')
-const DeviceInfo = require('../objects/DeviceInfo')
-const Stream = require('../objects/Stream')
const Logger = require('../Logger')
-const fs = require('../libs/fsExtra')
+const SocketAuthority = require('../SocketAuthority')
+const date = require('../libs/dateAndTime')
+const fs = require('../libs/fsExtra')
const uaParserJs = require('../libs/uaParser')
const requestIp = require('../libs/requestIp')
+const { PlayMethod } = require('../utils/constants')
+
+const PlaybackSession = require('../objects/PlaybackSession')
+const DeviceInfo = require('../objects/DeviceInfo')
+const Stream = require('../objects/Stream')
+
+
class PlaybackSessionManager {
- constructor(db, emitter, clientEmitter) {
+ constructor(db) {
this.db = db
this.StreamsPath = Path.join(global.MetadataPath, 'streams')
- this.emitter = emitter
- this.clientEmitter = clientEmitter
this.sessions = []
this.localSessionLock = {}
@@ -98,7 +100,7 @@ class PlaybackSessionManager {
if (wasUpdated) {
await this.db.updateEntity('user', user)
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
- this.clientEmitter(user.id, 'user_item_progress_updated', {
+ SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
id: itemProgress.id,
data: itemProgress.toJSON()
})
@@ -147,7 +149,7 @@ class PlaybackSessionManager {
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
} else {
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
- var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
+ var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
await stream.generatePlaylist()
stream.start() // Start transcode
@@ -167,7 +169,7 @@ class PlaybackSessionManager {
user.currentSessionId = newPlaybackSession.id
this.sessions.push(newPlaybackSession)
- this.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
}
@@ -193,7 +195,7 @@ class PlaybackSessionManager {
await this.db.updateEntity('user', user)
var itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
- this.clientEmitter(user.id, 'user_item_progress_updated', {
+ SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
id: itemProgress.id,
data: itemProgress.toJSON()
})
@@ -211,7 +213,7 @@ class PlaybackSessionManager {
await this.saveSession(session)
}
Logger.debug(`[PlaybackSessionManager] closeSession "${session.id}"`)
- this.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)
}
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index 59bc7ea4..74751d45 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -1,23 +1,24 @@
+const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const fs = require('../libs/fsExtra')
const { getPodcastFeed } = require('../utils/podcastUtils')
-const Logger = require('../Logger')
-
const { downloadFile, removeFile } = require('../utils/fileUtils')
const filePerms = require('../utils/filePerms')
const { levenshteinDistance } = require('../utils/index')
const opmlParser = require('../utils/parsers/parseOPML')
const prober = require('../utils/prober')
+
const LibraryFile = require('../objects/files/LibraryFile')
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
const AudioFile = require('../objects/files/AudioFile')
class PodcastManager {
- constructor(db, watcher, emitter, notificationManager) {
+ constructor(db, watcher, notificationManager) {
this.db = db
this.watcher = watcher
- this.emitter = emitter
this.notificationManager = notificationManager
this.downloadQueue = []
@@ -63,11 +64,11 @@ class PodcastManager {
async startPodcastEpisodeDownload(podcastEpisodeDownload) {
if (this.currentDownload) {
this.downloadQueue.push(podcastEpisodeDownload)
- this.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
+ SocketAuthority.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
return
}
- this.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
+ SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
this.currentDownload = podcastEpisodeDownload
// Ignores all added files to this dir
@@ -97,7 +98,7 @@ class PodcastManager {
this.currentDownload.setFinished(false)
}
- this.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
+ SocketAuthority.emitter('episode_download_finished', this.currentDownload.toJSONForClient())
this.watcher.removeIgnoreDir(this.currentDownload.libraryItem.path)
this.currentDownload = null
@@ -141,7 +142,7 @@ class PodcastManager {
libraryItem.updatedAt = Date.now()
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
@@ -230,7 +231,7 @@ class PodcastManager {
libraryItem.media.lastEpisodeCheck = Date.now()
libraryItem.updatedAt = Date.now()
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
return libraryItem.media.autoDownloadEpisodes
}
@@ -269,7 +270,7 @@ class PodcastManager {
libraryItem.media.lastEpisodeCheck = Date.now()
libraryItem.updatedAt = Date.now()
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
return newEpisodes
}
diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js
index ce15bbea..bd90b96c 100644
--- a/server/managers/RssFeedManager.js
+++ b/server/managers/RssFeedManager.js
@@ -1,13 +1,15 @@
const Path = require('path')
+
+const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const fs = require('../libs/fsExtra')
const Feed = require('../objects/Feed')
-const Logger = require('../Logger')
-// Not functional at the moment
class RssFeedManager {
- constructor(db, emitter) {
+ constructor(db) {
this.db = db
- this.emitter = emitter
+
this.feeds = {}
}
@@ -104,7 +106,7 @@ class RssFeedManager {
Logger.debug(`[RssFeedManager] Opened RSS feed ${feed.feedUrl}`)
await this.db.insertEntity('feed', feed)
- this.emitter('rss_feed_open', { id: feed.id, entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
+ SocketAuthority.emitter('rss_feed_open', { id: feed.id, entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
return feed
}
@@ -118,7 +120,7 @@ class RssFeedManager {
if (!this.feeds[id]) return
var feed = this.feeds[id]
await this.db.removeEntity('feed', id)
- this.emitter('rss_feed_closed', { id: feed.id, entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
+ SocketAuthority.emitter('rss_feed_closed', { id: feed.id, entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
delete this.feeds[id]
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
}
diff --git a/server/managers/TaskManager.js b/server/managers/TaskManager.js
index d962d0c8..38e8b580 100644
--- a/server/managers/TaskManager.js
+++ b/server/managers/TaskManager.js
@@ -1,19 +1,19 @@
-class TaskManager {
- constructor(emitter) {
- this.emitter = emitter
+const SocketAuthority = require('../SocketAuthority')
+class TaskManager {
+ constructor() {
this.tasks = []
}
addTask(task) {
this.tasks.push(task)
- this.emitter('task_started', task.toJSON())
+ SocketAuthority.emitter('task_started', task.toJSON())
}
taskFinished(task) {
if (this.tasks.some(t => t.id === task.id)) {
this.tasks = this.tasks.filter(t => t.id !== task.id)
- this.emitter('task_finished', task.toJSON())
+ SocketAuthority.emitter('task_finished', task.toJSON())
}
}
}
diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js
index 28a60e78..ed3a7e21 100644
--- a/server/objects/FeedEpisode.js
+++ b/server/objects/FeedEpisode.js
@@ -92,11 +92,15 @@ class FeedEpisode {
const media = libraryItem.media
const mediaMetadata = media.metadata
- var title = audioTrack.title
- if (libraryItem.media.chapters.length) {
- // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
- var matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
- if (matchingChapter && matchingChapter.title) title = matchingChapter.title
+ let title = audioTrack.title
+ if (libraryItem.media.tracks.length == 1) { // If audiobook is a single file, use book title instead of chapter/file title
+ title = libraryItem.media.metadata.title
+ } else {
+ if (libraryItem.media.chapters.length) {
+ // If audio track start and chapter start are within 1 seconds of eachother then use the chapter title
+ var matchingChapter = libraryItem.media.chapters.find(ch => Math.abs(ch.start - audioTrack.startOffset) < 1)
+ if (matchingChapter && matchingChapter.title) title = matchingChapter.title
+ }
}
this.id = String(audioTrack.index)
diff --git a/server/objects/Stream.js b/server/objects/Stream.js
index 19ea46e0..90403ea3 100644
--- a/server/objects/Stream.js
+++ b/server/objects/Stream.js
@@ -1,8 +1,12 @@
-const Ffmpeg = require('../libs/fluentFfmpeg')
+
const EventEmitter = require('events')
const Path = require('path')
-const fs = require('../libs/fsExtra')
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
+const fs = require('../libs/fsExtra')
+const Ffmpeg = require('../libs/fluentFfmpeg')
+
const { secondsToTimestamp } = require('../utils/index')
const { writeConcatFile } = require('../utils/ffmpegHelpers')
const { AudioMimeType } = require('../utils/constants')
@@ -10,14 +14,13 @@ const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
const AudioTrack = require('./files/AudioTrack')
class Stream extends EventEmitter {
- constructor(sessionId, streamPath, user, libraryItem, episodeId, startTime, clientEmitter, transcodeOptions = {}) {
+ constructor(sessionId, streamPath, user, libraryItem, episodeId, startTime, transcodeOptions = {}) {
super()
this.id = sessionId
this.user = user
this.libraryItem = libraryItem
this.episodeId = episodeId
- this.clientEmitter = clientEmitter
this.transcodeOptions = transcodeOptions
@@ -408,7 +411,7 @@ class Stream extends EventEmitter {
}
clientEmit(evtName, data) {
- if (this.clientEmitter) this.clientEmitter(this.user.id, evtName, data)
+ SocketAuthority.clientEmitter(this.user.id, evtName, data)
}
getAudioTrack() {
diff --git a/server/objects/user/MediaProgress.js b/server/objects/user/MediaProgress.js
index 70b2d678..3c4b4ca3 100644
--- a/server/objects/user/MediaProgress.js
+++ b/server/objects/user/MediaProgress.js
@@ -63,12 +63,12 @@ class MediaProgress {
this.isFinished = !!progress.isFinished || this.progress == 1
this.hideFromContinueListening = !!progress.hideFromContinueListening
this.lastUpdate = Date.now()
- this.startedAt = Date.now()
this.finishedAt = null
if (this.isFinished) {
- this.finishedAt = Date.now()
+ this.finishedAt = progress.finishedAt || Date.now()
this.progress = 1
}
+ this.startedAt = progress.startedAt || this.finishedAt || Date.now()
}
update(payload) {
@@ -95,7 +95,7 @@ class MediaProgress {
// If time remaining is less than 5 seconds then mark as finished
if ((this.progress >= 1 || (this.duration && !isNaN(timeRemaining) && timeRemaining < 5))) {
this.isFinished = true
- this.finishedAt = Date.now()
+ this.finishedAt = payload.finishedAt || Date.now()
this.progress = 1
} else if (this.progress < 1 && this.isFinished) {
this.isFinished = false
@@ -103,7 +103,7 @@ class MediaProgress {
}
if (!this.startedAt) {
- this.startedAt = Date.now()
+ this.startedAt = this.finishedAt || Date.now()
}
if (hasUpdates) {
if (payload.hideFromContinueListening === undefined) {
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index d296cf50..febcb4f3 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -1,8 +1,11 @@
const express = require('express')
const Path = require('path')
+
+const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
const fs = require('../libs/fsExtra')
const date = require('../libs/dateAndTime')
-const Logger = require('../Logger')
const LibraryController = require('../controllers/LibraryController')
const UserController = require('../controllers/UserController')
@@ -29,25 +32,22 @@ const Author = require('../objects/entities/Author')
const Series = require('../objects/entities/Series')
class ApiRouter {
- constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, rssFeedManager, cronManager, notificationManager, taskManager, getUsersOnline, emitter, clientEmitter) {
- this.db = db
- this.auth = auth
- this.scanner = scanner
- this.playbackSessionManager = playbackSessionManager
- this.abMergeManager = abMergeManager
- this.backupManager = backupManager
- this.coverManager = coverManager
- this.watcher = watcher
- this.cacheManager = cacheManager
- this.podcastManager = podcastManager
- this.audioMetadataManager = audioMetadataManager
- this.rssFeedManager = rssFeedManager
- this.cronManager = cronManager
- this.notificationManager = notificationManager
- this.taskManager = taskManager
- this.getUsersOnline = getUsersOnline
- this.emitter = emitter
- this.clientEmitter = clientEmitter
+ constructor(Server) {
+ this.db = Server.db
+ this.auth = Server.auth
+ this.scanner = Server.scanner
+ this.playbackSessionManager = Server.playbackSessionManager
+ this.abMergeManager = Server.abMergeManager
+ this.backupManager = Server.backupManager
+ this.coverManager = Server.coverManager
+ this.watcher = Server.watcher
+ this.cacheManager = Server.cacheManager
+ this.podcastManager = Server.podcastManager
+ this.audioMetadataManager = Server.audioMetadataManager
+ this.rssFeedManager = Server.rssFeedManager
+ this.cronManager = Server.cronManager
+ this.notificationManager = Server.notificationManager
+ this.taskManager = Server.taskManager
this.bookFinder = new BookFinder()
this.authorFinder = new AuthorFinder()
@@ -163,10 +163,11 @@ class ApiRouter {
//
// Backup Routes
//
- this.router.post('/backups', BackupController.create.bind(this))
- this.router.delete('/backups/:id', BackupController.delete.bind(this))
- this.router.get('/backups/:id/apply', BackupController.apply.bind(this))
- this.router.post('/backups/upload', BackupController.upload.bind(this))
+ this.router.get('/backups', BackupController.middleware.bind(this), BackupController.getAll.bind(this))
+ this.router.post('/backups', BackupController.middleware.bind(this), BackupController.create.bind(this))
+ this.router.delete('/backups/:id', BackupController.middleware.bind(this), BackupController.delete.bind(this))
+ this.router.get('/backups/:id/apply', BackupController.middleware.bind(this), BackupController.apply.bind(this))
+ this.router.post('/backups/upload', BackupController.middleware.bind(this), BackupController.upload.bind(this))
//
// File System Routes
@@ -261,13 +262,13 @@ class ApiRouter {
async getDirectories(dir, relpath, excludedDirs, level = 0) {
try {
- var paths = await fs.readdir(dir)
+ const paths = await fs.readdir(dir)
- var dirs = await Promise.all(paths.map(async dirname => {
- var fullPath = Path.join(dir, dirname)
- var path = Path.join(relpath, dirname)
+ let dirs = await Promise.all(paths.map(async dirname => {
+ const fullPath = Path.join(dir, 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') {
return {
path,
@@ -292,13 +293,13 @@ class ApiRouter {
// Helper Methods
//
userJsonWithItemProgressDetails(user, hideRootToken = false) {
- var json = user.toJSONForBrowser()
+ const json = user.toJSONForBrowser()
if (json.type === 'root' && hideRootToken) {
json.token = ''
}
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) {
Logger.warn('[ApiRouter] Library item not found for users progress ' + lip.libraryItemId)
lip.media = null
@@ -325,34 +326,21 @@ class ApiRouter {
async handleDeleteLibraryItem(libraryItem) {
// Remove libraryItem from users
for (let i = 0; i < this.db.users.length; i++) {
- var user = this.db.users[i]
- var madeUpdates = user.removeMediaProgressForLibraryItem(libraryItem.id)
- if (madeUpdates) {
+ const user = this.db.users[i]
+ if (user.removeMediaProgressForLibraryItem(libraryItem.id)) {
await this.db.updateEntity('user', user)
}
}
- // remove any streams open for this audiobook
- // 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)
- // }
- // }
+ // TODO: Remove open sessions for library item
// 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++) {
- var collection = collectionsWithBook[i]
+ const collection = collectionsWithBook[i]
collection.removeBook(libraryItem.id)
await this.db.updateEntity('collection', collection)
- this.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.libraryItems))
+ SocketAuthority.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.libraryItems))
}
// purge cover cache
@@ -360,34 +348,32 @@ class ApiRouter {
await this.cacheManager.purgeCoverCache(libraryItem.id)
}
- var json = libraryItem.toJSONExpanded()
await this.db.removeLibraryItem(libraryItem.id)
- this.emitter('item_removed', json)
+ SocketAuthority.emitter('item_removed', libraryItem.toJSONExpanded())
}
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)
}
async getAllSessionsWithUserData() {
- var sessions = await this.db.getAllSessions()
+ const sessions = await this.db.getAllSessions()
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
return sessions.map(se => {
- var user = this.db.users.find(u => u.id === se.userId)
- var _se = {
+ const user = this.db.users.find(u => u.id === se.userId)
+ return {
...se,
user: user ? { id: user.id, username: user.username } : null
}
- return _se
})
}
async getUserListeningStatsHelpers(userId) {
const today = date.format(new Date(), 'YYYY-MM-DD')
- var listeningSessions = await this.getUserListeningSessionsHelper(userId)
- var listeningStats = {
+ const listeningSessions = await this.getUserListeningSessionsHelper(userId)
+ const listeningStats = {
totalTime: 0,
items: {},
days: {},
@@ -396,7 +382,7 @@ class ApiRouter {
recentSessions: listeningSessions.slice(0, 10)
}
listeningSessions.forEach((s) => {
- var sessionTimeListening = s.timeListening
+ let sessionTimeListening = s.timeListening
if (typeof sessionTimeListening == 'string') {
sessionTimeListening = Number(sessionTimeListening)
}
@@ -431,15 +417,15 @@ class ApiRouter {
async createAuthorsAndSeriesForItemUpdate(mediaPayload) {
if (mediaPayload.metadata) {
- var mediaMetadata = mediaPayload.metadata
+ const mediaMetadata = mediaPayload.metadata
// Create new authors if in payload
if (mediaMetadata.authors && mediaMetadata.authors.length) {
// TODO: validate authors
- var newAuthors = []
+ const newAuthors = []
for (let i = 0; i < mediaMetadata.authors.length; i++) {
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) {
author = new Author()
author.setData(mediaMetadata.authors[i])
@@ -453,17 +439,17 @@ class ApiRouter {
}
if (newAuthors.length) {
await this.db.insertEntities('author', newAuthors)
- this.emitter('authors_added', newAuthors)
+ SocketAuthority.emitter('authors_added', newAuthors)
}
}
// Create new series if in payload
if (mediaMetadata.series && mediaMetadata.series.length) {
// TODO: validate series
- var newSeries = []
+ const newSeries = []
for (let i = 0; i < mediaMetadata.series.length; i++) {
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) {
seriesItem = new Series()
seriesItem.setData(mediaMetadata.series[i])
@@ -477,7 +463,7 @@ class ApiRouter {
}
if (newSeries.length) {
await this.db.insertEntities('series', newSeries)
- this.emitter('authors_added', newSeries)
+ SocketAuthority.emitter('authors_added', newSeries)
}
}
}
diff --git a/server/routers/HlsRouter.js b/server/routers/HlsRouter.js
index 80a0913e..58896097 100644
--- a/server/routers/HlsRouter.js
+++ b/server/routers/HlsRouter.js
@@ -1,14 +1,17 @@
const express = require('express')
const Path = require('path')
-const fs = require('../libs/fsExtra')
+
const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
+
+const fs = require('../libs/fsExtra')
+
class HlsRouter {
- constructor(db, auth, playbackSessionManager, emitter) {
+ constructor(db, auth, playbackSessionManager) {
this.db = db
this.auth = auth
this.playbackSessionManager = playbackSessionManager
- this.emitter = emitter
this.router = express()
this.init()
@@ -49,7 +52,7 @@ class HlsRouter {
if (startTimeForReset) {
// HLS.js will restart the stream at the new time
Logger.info(`[HlsRouter] Resetting Stream - notify client @${startTimeForReset}s`)
- this.emitter('stream_reset', {
+ SocketAuthority.emitter('stream_reset', {
startTime: startTimeForReset,
streamId: stream.id
})
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 16d1355a..68dfdd8f 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -1,8 +1,9 @@
const fs = require('../libs/fsExtra')
const Path = require('path')
+const Logger = require('../Logger')
+const SocketAuthority = require('../SocketAuthority')
// Utils
-const Logger = require('../Logger')
const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder } = require('../utils/scandir')
const { comparePaths } = require('../utils/index')
const { getIno } = require('../utils/fileUtils')
@@ -20,12 +21,11 @@ const Author = require('../objects/entities/Author')
const Series = require('../objects/entities/Series')
class Scanner {
- constructor(db, coverManager, emitter) {
+ constructor(db, coverManager) {
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
this.db = db
this.coverManager = coverManager
- this.emitter = emitter
this.cancelLibraryScan = {}
this.librariesScanning = []
@@ -113,7 +113,7 @@ class Scanner {
}
if (hasUpdated) {
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
await this.db.updateLibraryItem(libraryItem)
return ScanResult.UPDATED
}
@@ -139,7 +139,7 @@ class Scanner {
libraryScan.verbose = false
this.librariesScanning.push(libraryScan.getScanEmitData)
- this.emitter('scan_start', libraryScan.getScanEmitData)
+ SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData)
Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`)
@@ -158,11 +158,11 @@ class Scanner {
if (canceled && !libraryScan.totalResults) {
var emitData = libraryScan.getScanEmitData
emitData.results = null
- this.emitter('scan_complete', emitData)
+ SocketAuthority.emitter('scan_complete', emitData)
return
}
- this.emitter('scan_complete', libraryScan.getScanEmitData)
+ SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)
if (libraryScan.totalResults) {
libraryScan.saveLog(this.ScanLogPath)
@@ -302,7 +302,7 @@ class Scanner {
async updateLibraryItemChunk(itemsToUpdate) {
await this.db.updateLibraryItems(itemsToUpdate)
- this.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter('items_updated', itemsToUpdate.map(li => li.toJSONExpanded()))
}
async rescanLibraryItemDataChunk(itemDataToRescan, libraryScan) {
@@ -320,7 +320,7 @@ class Scanner {
if (itemsUpdated.length) {
libraryScan.resultsUpdated += itemsUpdated.length
await this.db.updateLibraryItems(itemsUpdated)
- this.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
}
}
@@ -337,7 +337,7 @@ class Scanner {
libraryScan.resultsAdded += newLibraryItems.length
await this.db.insertLibraryItems(newLibraryItems)
- this.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded()))
+ SocketAuthority.emitter('items_added', newLibraryItems.map(li => li.toJSONExpanded()))
}
async rescanLibraryItem(libraryItemCheckData, libraryScan) {
@@ -458,7 +458,7 @@ class Scanner {
})
if (newAuthors.length) {
await this.db.insertEntities('author', newAuthors)
- this.emitter('authors_added', newAuthors.map(au => au.toJSON()))
+ SocketAuthority.emitter('authors_added', newAuthors.map(au => au.toJSON()))
}
}
if (libraryItem.media.metadata.series.some(se => se.id.startsWith('new'))) {
@@ -479,7 +479,7 @@ class Scanner {
})
if (newSeries.length) {
await this.db.insertEntities('series', newSeries)
- this.emitter('series_added', newSeries.map(se => se.toJSON()))
+ SocketAuthority.emitter('series_added', newSeries.map(se => se.toJSON()))
}
}
}
@@ -602,7 +602,7 @@ class Scanner {
Logger.info(`[Scanner] Scanning file update group and library item was deleted "${existingLibraryItem.media.metadata.title}" - marking as missing`)
existingLibraryItem.setMissing()
await this.db.updateLibraryItem(existingLibraryItem)
- this.emitter('item_updated', existingLibraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded())
itemGroupingResults[itemDir] = ScanResult.REMOVED
continue;
@@ -629,7 +629,7 @@ class Scanner {
if (newLibraryItem) {
await this.createNewAuthorsAndSeries(newLibraryItem)
await this.db.insertLibraryItem(newLibraryItem)
- this.emitter('item_added', newLibraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_added', newLibraryItem.toJSONExpanded())
}
itemGroupingResults[itemDir] = newLibraryItem ? ScanResult.ADDED : ScanResult.NOTHING
}
@@ -747,7 +747,7 @@ class Scanner {
}
await this.db.updateLibraryItem(libraryItem)
- this.emitter('item_updated', libraryItem.toJSONExpanded())
+ SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
}
return {
@@ -846,7 +846,7 @@ class Scanner {
author = new Author()
author.setData({ name: authorName })
await this.db.insertEntity('author', author)
- this.emitter('author_added', author)
+ SocketAuthority.emitter('author_added', author)
}
authorPayload.push(author.toJSONMinimal())
}
@@ -864,7 +864,7 @@ class Scanner {
seriesItem = new Series()
seriesItem.setData({ name: seriesMatchItem.series })
await this.db.insertEntity('series', seriesItem)
- this.emitter('series_added', seriesItem)
+ SocketAuthority.emitter('series_added', seriesItem)
}
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
}
@@ -955,7 +955,7 @@ class Scanner {
var libraryScan = new LibraryScan()
libraryScan.setData(library, null, 'match')
this.librariesScanning.push(libraryScan.getScanEmitData)
- this.emitter('scan_start', libraryScan.getScanEmitData)
+ SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData)
Logger.info(`[Scanner] matchLibraryItems: Starting library match scan ${libraryScan.id} for ${libraryScan.libraryName}`)
@@ -987,14 +987,14 @@ class Scanner {
delete this.cancelLibraryScan[libraryScan.libraryId]
var scanData = libraryScan.getScanEmitData
scanData.results = false
- this.emitter('scan_complete', scanData)
+ SocketAuthority.emitter('scan_complete', scanData)
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
return
}
}
this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
- this.emitter('scan_complete', libraryScan.getScanEmitData)
+ SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData)
}
probeAudioFileWithTone(audioFile) {