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 @@ </td> <td class="text-sm">{{ user.type }}</td> <td class="hidden lg:table-cell"> - <div v-if="usersOnline[user.id] && usersOnline[user.id].session && usersOnline[user.id].session.libraryItem && usersOnline[user.id].session.libraryItem.media"> - <p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p> - </div> - <div v-else-if="user.mostRecent"> - <p class="truncate text-xs">Last: {{ user.mostRecent.metadata.title }}</p> + <div v-if="usersOnline[user.id]"> + <p v-if="usersOnline[user.id].session && usersOnline[user.id].session.libraryItem" class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p> + <p v-else-if="usersOnline[user.id].mostRecent && usersOnline[user.id].mostRecent.media" class="truncate text-xs">Last: {{ usersOnline[user.id].mostRecent.media.metadata.title }}</p> </div> </td> <td class="text-xs font-mono hidden sm:table-cell"> @@ -81,7 +79,7 @@ export default { }, usersOnline() { var usermap = {} - this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session })) + this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u)) return usermap } }, diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 4d9d1438..bb93a054 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -132,14 +132,8 @@ export default { } }) - if (payload.backups && payload.backups.length) { - this.$store.commit('setBackups', payload.backups) - } if (payload.usersOnline) { - this.$store.commit('users/resetUsers') - payload.usersOnline.forEach((user) => { - this.$store.commit('users/updateUser', user) - }) + this.$store.commit('users/setUsersOnline', payload.usersOnline) } this.$eventBus.$emit('socket_init') @@ -286,13 +280,13 @@ export default { } }, userOnline(user) { - this.$store.commit('users/updateUser', user) + this.$store.commit('users/updateUserOnline', user) }, userOffline(user) { - this.$store.commit('users/removeUser', user) + this.$store.commit('users/removeUserOnline', user) }, userStreamUpdate(user) { - this.$store.commit('users/updateUser', user) + this.$store.commit('users/updateUserOnline', user) }, userMediaProgressUpdate(payload) { this.$store.commit('user/updateMediaProgress', payload) @@ -333,6 +327,9 @@ export default { this.$toast.info(toast) } }, + adminMessageEvt(message) { + this.$toast.info(message) + }, initializeSocket() { this.socket = this.$nuxtSocket({ name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod', @@ -345,6 +342,7 @@ export default { this.$root.socket = this.socket console.log('Socket initialized') + // Pre-defined socket events this.socket.on('connect', this.connect) this.socket.on('connect_error', this.connectError) this.socket.on('disconnect', this.disconnect) @@ -353,6 +351,7 @@ export default { this.socket.io.on('reconnect_error', this.reconnectError) this.socket.io.on('reconnect_failed', this.reconnectFailed) + // Event received after authorizing socket this.socket.on('init', this.init) // Stream Listeners @@ -403,6 +402,8 @@ export default { this.socket.on('backup_applied', this.backupApplied) this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete) + + this.socket.on('admin_message', this.adminMessageEvt) }, showUpdateToast(versionData) { var ignoreVersion = localStorage.getItem('ignoreVersion') diff --git a/client/store/index.js b/client/store/index.js index 4ddb5a3c..9b27abd4 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -21,7 +21,6 @@ export const state = () => ({ processingBatch: false, previousPath: '/', showExperimentalFeatures: false, - backups: [], bookshelfBookIds: [], openModal: null, innerModalOpen: false, @@ -245,9 +244,6 @@ export const mutations = { state.showExperimentalFeatures = val localStorage.setItem('experimental', val ? 1 : 0) }, - setBackups(state, val) { - state.backups = val.sort((a, b) => b.createdAt - a.createdAt) - }, setOpenModal(state, val) { state.openModal = val }, diff --git a/client/store/users.js b/client/store/users.js index 131b7d8c..2439ee31 100644 --- a/client/store/users.js +++ b/client/store/users.js @@ -1,11 +1,11 @@ export const state = () => ({ - users: [] + usersOnline: [] }) export const getters = { getIsUserOnline: state => id => { - return state.users.find(u => u.id === id) + return state.usersOnline.find(u => u.id === id) } } @@ -14,18 +14,18 @@ export const actions = { } export const mutations = { - resetUsers(state) { - state.users = [] + setUsersOnline(state, usersOnline) { + state.usersOnline = usersOnline }, - updateUser(state, user) { - var index = state.users.findIndex(u => u.id === user.id) + updateUserOnline(state, user) { + var index = state.usersOnline.findIndex(u => u.id === user.id) if (index >= 0) { - state.users.splice(index, 1, user) + state.usersOnline.splice(index, 1, user) } else { - state.users.push(user) + state.usersOnline.push(user) } }, - removeUser(state, user) { - state.users = state.users.filter(u => u.id !== user.id) + removeUserOnline(state, user) { + state.usersOnline = state.usersOnline.filter(u => u.id !== user.id) } } \ No newline at end of file diff --git a/client/strings/pl.json b/client/strings/pl.json index 05b3d17d..a9fdffb0 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -13,7 +13,7 @@ "ButtonCheckAndDownloadNewEpisodes": "Sprawdź i pobierz nowe odcinki", "ButtonChooseAFolder": "Wybierz folder", "ButtonChooseFiles": "Wybierz pliki", - "ButtonClearFilter": "Clear Filter", + "ButtonClearFilter": "Wyczyść filtr", "ButtonCloseFeed": "Zamknij kanał", "ButtonCollections": "Kolekcje", "ButtonConfigureScanner": "Configure Scanner", @@ -44,8 +44,8 @@ "ButtonPurgeAllCache": "Wyczyść dane tymczasowe", "ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji", "ButtonPurgeMediaProgress": "Wyczyść postęp", - "ButtonQueueAddItem": "Add to queue", - "ButtonQueueRemoveItem": "Remove from queue", + "ButtonQueueAddItem": "Dodaj do kolejki", + "ButtonQueueRemoveItem": "Usuń z kolejki", "ButtonQuickMatch": "Szybkie dopasowanie", "ButtonRead": "Czytaj", "ButtonRemove": "Usuń", @@ -68,7 +68,7 @@ "ButtonShow": "Pokaż", "ButtonStartM4BEncode": "Eksportuj jako plik M4B", "ButtonStartMetadataEmbed": "Osadź metadane", - "ButtonSubmit": "Zgłoś", + "ButtonSubmit": "Zaloguj", "ButtonUpload": "Wgraj", "ButtonUploadBackup": "Wgraj kopię zapasową", "ButtonUploadCover": "Wgraj okładkę", @@ -96,7 +96,7 @@ "HeaderLastListeningSession": "Ostatnio odtwarzana sesja", "HeaderLatestEpisodes": "Najnowsze odcinki", "HeaderLibraries": "Biblioteki", - "HeaderLibraryFiles": "Library Files", + "HeaderLibraryFiles": "Pliki w bibliotece", "HeaderLibraryStats": "Statystyki biblioteki", "HeaderListeningSessions": "Sesje słuchania", "HeaderListeningStats": "Statystyki odtwarzania", @@ -144,14 +144,14 @@ "LabelAccountTypeGuest": "Gość", "LabelAccountTypeUser": "Użytkownik", "LabelActivity": "Aktywność", - "LabelAddedAt": "Added At", + "LabelAddedAt": "Dodano", "LabelAddToCollection": "Dodaj do kolekcji", "LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji", "LabelAll": "All", "LabelAllUsers": "Wszyscy użytkownicy", "LabelAuthor": "Autor", - "LabelAuthorFirstLast": "Author (First Last)", - "LabelAuthorLastFirst": "Author (Last, First)", + "LabelAuthorFirstLast": "Autor (Rosnąco)", + "LabelAuthorLastFirst": "Author (Malejąco)", "LabelAuthors": "Autorzy", "LabelAutoDownloadEpisodes": "Automatyczne pobieranie odcinków", "LabelBackToUser": "Powrót", @@ -165,7 +165,7 @@ "LabelChangePassword": "Zmień hasło", "LabelChaptersFound": "Znalezione rozdziały", "LabelChapterTitle": "Tytuł rozdziału", - "LabelClosePlayer": "Close player", + "LabelClosePlayer": "Zamknij odtwarzacz", "LabelCollapseSeries": "Podsumuj serię", "LabelCollections": "Kolekcje", "LabelComplete": "Ukończone", @@ -198,15 +198,15 @@ "LabelExplicit": "Nieprzyzwoite", "LabelFeedURL": "URL kanału", "LabelFile": "Plik", - "LabelFileBirthtime": "File Birthtime", - "LabelFileModified": "File Modified", + "LabelFileBirthtime": "Data utworzenia pliku", + "LabelFileModified": "Data modyfikacji pliku", "LabelFilename": "Nazwa pliku", "LabelFilterByUser": "Filtruj według danego użytkownika", "LabelFindEpisodes": "Znajdź odcinki", "LabelFinished": "Zakończone", "LabelFolder": "Folder", "LabelFolders": "Foldery", - "LabelGenre": "Genre", + "LabelGenre": "Gatunek", "LabelGenres": "Gatunki", "LabelHardDeleteFile": "Usuń trwale plik", "LabelHour": "Godzina", @@ -215,14 +215,14 @@ "LabelIncomplete": "Nieukończone", "LabelInProgress": "W trakcie", "LabelInterval": "Interwał", - "LabelIntervalCustomDailyWeekly": "Custom daily/weekly", - "LabelIntervalEvery12Hours": "Every 12 hours", - "LabelIntervalEvery15Minutes": "Every 15 minutes", - "LabelIntervalEvery2Hours": "Every 2 hours", - "LabelIntervalEvery30Minutes": "Every 30 minutes", - "LabelIntervalEvery6Hours": "Every 6 hours", - "LabelIntervalEveryDay": "Every day", - "LabelIntervalEveryHour": "Every hour", + "LabelIntervalCustomDailyWeekly": "Niestandardowy dzienny/tygodniowy", + "LabelIntervalEvery12Hours": "Co 12 godzin", + "LabelIntervalEvery15Minutes": "Co 15 minut", + "LabelIntervalEvery2Hours": "Co 2 godziny", + "LabelIntervalEvery30Minutes": "Co 30 minut", + "LabelIntervalEvery6Hours": "Co 6 godzin", + "LabelIntervalEveryDay": "Każdego dnia", + "LabelIntervalEveryHour": "Każdej godziny", "LabelInvalidParts": "Nieprawidłowe części", "LabelItem": "Pozycja", "LabelLanguage": "Język", @@ -238,8 +238,8 @@ "LabelLimit": "Limit", "LabelListenAgain": "Słuchaj ponownie", "LabelLogLevelDebug": "Debug", - "LabelLogLevelInfo": "Info", - "LabelLogLevelWarn": "Warn", + "LabelLogLevelInfo": "Informacja", + "LabelLogLevelWarn": "Ostrzeżenie", "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", "LabelMarkSeries": "Oznacz serię", "LabelMediaPlayer": "Odtwarzacz", @@ -268,9 +268,9 @@ "LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień", "LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.", "LabelNotificationTitleTemplate": "Szablon tytułu powiadmienia", - "LabelNotStarted": "Not Started", - "LabelNumberOfBooks": "Number of Books", - "LabelNumberOfEpisodes": "# of Episodes", + "LabelNotStarted": "Nie rozpoęczto", + "LabelNumberOfBooks": "Liczba książek", + "LabelNumberOfEpisodes": "# odcinków", "LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelPassword": "Hasło", "LabelPath": "Ścieżka", @@ -296,7 +296,7 @@ "LabelRegion": "Region", "LabelReleaseDate": "Data wydania", "LabelRemoveCover": "Remove cover", - "LabelRSSFeedOpen": "RSS Feed Open", + "LabelRSSFeedOpen": "RSS Feed otwarty", "LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedURL": "URL kanały RSS", "LabelSearchTerm": "Wyszukiwanie frazy", @@ -306,7 +306,7 @@ "LabelSequence": "Kolejność", "LabelSeries": "Serie", "LabelSeriesName": "Nazwy serii", - "LabelSeriesProgress": "Series Progress", + "LabelSeriesProgress": "Postęp w serii", "LabelSettingsBookshelfViewHelp": "Widok półki z ksiązkami", "LabelSettingsChromecastSupport": "Wsparcie Chromecast", "LabelSettingsDateFormat": "Format daty", @@ -343,7 +343,7 @@ "LabelSettingsStoreMetadataWithItemHelp": "Domyślnie metadane są przechowywane w folderze /metadata/items, włączenie tej opcji spowoduje, że okładka będzie przechowywana w folderze ksiązki. Tylko jedna okładka o nazwie pliku \"cover\" będzie przechowywana. Rozszerzenie pliku metadanych: .abs", "LabelShowAll": "Pokaż wszystko", "LabelSize": "Rozmiar", - "LabelSleepTimer": "Sleep timer", + "LabelSleepTimer": "Wyłącznik czasowy", "LabelStart": "Rozpocznij", "LabelStarted": "Rozpoczęty", "LabelStartedAt": "Rozpoczęto", @@ -360,8 +360,8 @@ "LabelStatsItemsInLibrary": "Pozycje w bibliotece", "LabelStatsMinutes": "Minuty", "LabelStatsMinutesListening": "Minuty odtwarzania", - "LabelStatsOverallDays": "Overall Days", - "LabelStatsOverallHours": "Overall Hours", + "LabelStatsOverallDays": "Całkowity czas (dni)", + "LabelStatsOverallHours": "Całkowity czas (godziny)", "LabelStatsWeekListening": "Tydzień odtwarzania", "LabelSubtitle": "Podtytuł", "LabelSupportedFileTypes": "Obsługiwane typy plików", @@ -373,13 +373,13 @@ "LabelTimeRemaining": "Pozostało {0}", "LabelTimeToShift": "Czas do przesunięcia w sekundach", "LabelTitle": "Tytuł", - "LabelToolsEmbedMetadata": "Embed Metadata", - "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.", - "LabelToolsMakeM4b": "Make M4B Audiobook File", - "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.", - "LabelToolsSplitM4b": "Split M4B to MP3's", - "LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.", - "LabelTotalDuration": "Total Duration", + "LabelToolsEmbedMetadata": "Załącz metadane", + "LabelToolsEmbedMetadataDescription": "Załącz metadane do plików audio (okładkę oraz znaczniki rozdziałów)", + "LabelToolsMakeM4b": "Generuj plik M4B", + "LabelToolsMakeM4bDescription": "Tworzy plik w formacie .M4B, który zawiera metadane, okładkę oraz rozdziały.", + "LabelToolsSplitM4b": "Podziel plik .M4B na pliki .MP3", + "LabelToolsSplitM4bDescription": "Podziel plik .M4B na pliki .MP3 na rozdziały z załączonymi metadanymi oraz okładką.", + "LabelTotalDuration": "TCałkowita długość", "LabelTotalTimeListened": "Całkowity czas odtwarzania", "LabelTrackFromFilename": "Ścieżka z nazwy pliku", "LabelTrackFromMetadata": "Ścieżka z metadanych", @@ -387,33 +387,33 @@ "LabelUnknown": "Nieznany", "LabelUpdateCover": "Zaktalizuj odkładkę", "LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania", - "LabelUpdatedAt": "Zaktualizaowano", + "LabelUpdatedAt": "Zaktualizowano", "LabelUpdateDetails": "Zaktualizuj szczegóły", "LabelUpdateDetailsHelp": "Umożliwienie nadpisania istniejących szczegółów dla wybranych książek w przypadku znalezienia dopasowania", "LabelUploaderDragAndDrop": "Przeciągnij i puść foldery lub pliki", "LabelUploaderDropFiles": "Puść pliki", - "LabelUseChapterTrack": "Use chapter track", + "LabelUseChapterTrack": "Użyj ścieżki rozdziału", "LabelUseFullTrack": "Użycie ścieżki rozdziału", "LabelUser": "Użytkownik", "LabelUsername": "Nazwa użytkownika", "LabelValue": "Wartość", "LabelVersion": "Wersja", - "LabelViewBookmarks": "View bookmarks", - "LabelViewChapters": "View chapters", - "LabelViewQueue": "View player queue", - "LabelVolume": "Volume", + "LabelViewBookmarks": "Wyświetlaj zakładki", + "LabelViewChapters": "Wyświetlaj rozdziały", + "LabelViewQueue": "Wyświetlaj kolejkę odtwarzania", + "LabelVolume": "Głośność", "LabelWeekdaysToRun": "Dni tygodnia", "LabelYourAudiobookDuration": "Czas trwania audiobooka", "LabelYourBookmarks": "Twoje zakładki", "LabelYourProgress": "Twój postęp", - "MessageAppriseDescription": "To use this feature you will need to have an instance of <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at <code>http://192.168.1.1:8337</code> then you would put <code>http://192.168.1.1:8337/notify</code>.", + "MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> albo innego rozwiązania, które obsługuje schemat zapytań Apprise. <br />URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem <code>http://192.168.1.1:8337</code> to wpisany tutaj URL powinien mieć postać: <code>http://192.168.1.1:8337/notify</code>.", "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) {