diff --git a/client/package-lock.json b/client/package-lock.json index ba071852b..79ac53250 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.34.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 71d64f60a..dd0f3a0c9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.35.0", + "version": "2.34.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/client/strings/ar.json b/client/strings/ar.json index dc5ef1762..a176b9d8b 100644 --- a/client/strings/ar.json +++ b/client/strings/ar.json @@ -244,8 +244,6 @@ "LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك", "LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.", "LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.", - "LabelApiKeyUser": "التصرف بالنيابة عن مستخدم", - "LabelApiKeyUserDescription": "مفتاح API سيمتلك نفس صلاحيات المستخدم الذي ينوب عنه ، سيظهر بالسجلات وكأن المستخدم قام بالطلب.", "LabelApiToken": "رمز API", "LabelAppend": "إلحاق", "LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)", @@ -295,7 +293,6 @@ "LabelContinueListening": "استمرار الاستماع", "LabelContinueReading": "استمرار القراءة", "LabelContinueSeries": "استمرار المسلسلات", - "LabelCorsAllowed": "CORS Origins مسموح", "LabelCover": "الغلاف", "LabelCoverImageURL": "رابط صورة الغلاف", "LabelCoverProvider": "مزود الغلاف", @@ -429,9 +426,6 @@ "LabelLibraryFilterSublistEmpty": "لا يوجد {0}", "LabelLibraryItem": "عنصر المكتبة", "LabelLibraryName": "اسم المكتبة", - "LabelLibrarySortByProgress": "المرحلة: الأحدث", - "LabelLibrarySortByProgressFinished": "المرحلة: تم الانتهاء", - "LabelLibrarySortByProgressStarted": "المرحلة: تم البدء", "LabelLimit": "حد", "LabelLineSpacing": "تباعد الأسطر", "LabelListenAgain": "الاستماع مجدداً", diff --git a/client/strings/be.json b/client/strings/be.json index d3aa42cf1..b4e4df86a 100644 --- a/client/strings/be.json +++ b/client/strings/be.json @@ -88,7 +88,7 @@ "ButtonResetToDefault": "Скінуць да прадвызначаных", "ButtonRestore": "Аднавіць", "ButtonSave": "Захаваць", - "ButtonSaveAndClose": "Захаваць і закрыць", + "ButtonSaveAndClose": "Захаваць і зачыніць", "ButtonSaveTracklist": "Захаваць спіс трэкаў", "ButtonScan": "Сканаваць", "ButtonScanLibrary": "Сканіраваць бібліятэку", @@ -284,7 +284,7 @@ "LabelChaptersFound": "раздзелаў знойдзена", "LabelClickForMoreInfo": "Націсніце для больш падрабязнай інфармацыі", "LabelClickToUseCurrentValue": "Націсніце, каб выкарыстоўваць бягучае значэнне", - "LabelClosePlayer": "Закрыць прайгравальнік", + "LabelClosePlayer": "Зачыніць прайгравальнік", "LabelCodec": "Кодэк", "LabelCollapseSeries": "Згарнуць серыі", "LabelCollapseSubSeries": "Згарнуць падсерыі", diff --git a/client/strings/bg.json b/client/strings/bg.json index c34188a54..460f0ff83 100644 --- a/client/strings/bg.json +++ b/client/strings/bg.json @@ -752,7 +752,7 @@ "MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове", "MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"", "MessageBookshelfNoResultsForQuery": "Няма резултати от заявката", - "MessageBookshelfNoSeries": "Нямате поредица", + "MessageBookshelfNoSeries": "Нямаш сеЗЙ", "MessageBulkChapterPattern": "Колко глави искате да добавите, използвайки тази схема за номериране?", "MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига", "MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0", @@ -1018,50 +1018,18 @@ "ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди", "ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.", "ToastChaptersHaveErrors": "Главите имат грешки", - "ToastChaptersInvalidShiftAmountLast": "Невалидно време за преместване. Началният час на последната глава ще превиши общата продължителност на аудиокнигата.", - "ToastChaptersInvalidShiftAmountStart": "Невалидно време за преместване. Първата глава ще има нулева или отрицателна дължина и ще бъде презаписана от втората глава. Увеличете началното време на втората глава.", "ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия", - "ToastChaptersRemoved": "Главите са премахнати", - "ToastChaptersUpdated": "Главите са актуализирани", - "ToastCollectionItemsAddFailed": "Неуспешно добавяне на елемент(и) към колекцията", "ToastCollectionRemoveSuccess": "Колекцията е премахната", "ToastCollectionUpdateSuccess": "Колекцията е обновена", - "ToastConnectionNotAvailable": "Няма връзка. Моля, опитайте отново по-късно", - "ToastCoverSearchFailed": "Търсенето на корица е неуспешно", - "ToastCoverUpdateFailed": "Обновяването на корицата е неуспешно", - "ToastDateTimeInvalidOrIncomplete": "Датата и часът са невалидни или непълни", "ToastDeleteFileFailed": "Неуспешно изтриване на файла", "ToastDeleteFileSuccess": "Успешно изтриване на файла", - "ToastDeviceAddFailed": "Неуспешно добавяне на устройство", - "ToastDeviceNameAlreadyExists": "Вече съществува четец с това име", - "ToastDeviceTestEmailFailed": "Неуспешно изпращане на тестов имейл", - "ToastDeviceTestEmailSuccess": "Тестовият имейл е изпратен", - "ToastEmailSettingsUpdateSuccess": "Имейл настройките са актуализирани", - "ToastEncodeCancelFailed": "Неуспешно отменяне на кодирането", - "ToastEncodeCancelSucces": "Кодирането е отменено", - "ToastEpisodeDownloadQueueClearFailed": "Неуспешно изчистване на опашката", - "ToastEpisodeDownloadQueueClearSuccess": "Опашката за изтегляне на епизоди е изчистена", - "ToastEpisodeUpdateSuccess": "{0} епизода са актуализирани", - "ToastErrorCannotShare": "Не може да се споделя директно от това устройство", - "ToastFailedToCreate": "Неуспешно създаване", - "ToastFailedToDelete": "Неуспешно изтриване", "ToastFailedToLoadData": "Неуспешно зареждане на данни", - "ToastFailedToMatch": "Неуспешно съвпадение", - "ToastFailedToShare": "Неуспешно споделяне", - "ToastFailedToUpdate": "Неуспешно актуализиране", - "ToastInvalidImageUrl": "Невалиден URL адрес на изображение", - "ToastInvalidMaxEpisodesToDownload": "Невалиден максимален брой епизоди за изтегляне", - "ToastInvalidUrl": "Невалиден URL адрес", - "ToastInvalidUrls": "Един или повече URL адреси са невалидни", "ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена", - "ToastItemDeletedFailed": "Неуспешно изтриване на елемента", - "ToastItemDeletedSuccess": "Елементът е изтрит", "ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени", "ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено", "ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен", "ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено", "ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен", - "ToastItemUpdateSuccess": "Елементът е актуализиран", "ToastLibraryCreateFailed": "Неуспешно създаване на библиотека", "ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена", "ToastLibraryDeleteFailed": "Неуспешно изтриване на библиотека", @@ -1069,97 +1037,28 @@ "ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране", "ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано", "ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена", - "ToastMatchAllAuthorsFailed": "Неуспешно съвпадение на всички автори", - "ToastMetadataFilesRemovedError": "Грешка при премахване на metadata.{0} файлове", - "ToastMetadataFilesRemovedNoneFound": "Не са намерени metadata.{0} файлове в библиотеката", - "ToastMetadataFilesRemovedNoneRemoved": "Не са премахнати metadata.{0} файлове", - "ToastMetadataFilesRemovedSuccess": "Премахнати са {0} файла metadata.{1}", - "ToastMustHaveAtLeastOnePath": "Трябва да има поне един път", - "ToastNameEmailRequired": "Изискват се име и имейл", - "ToastNameRequired": "Изисква се име", - "ToastNewApiKeyUserError": "Трябва да изберете потребител", - "ToastNewEpisodesFound": "Намерени са {0} нови епизода", - "ToastNewUserCreatedFailed": "Неуспешно създаване на акаунт: „{0}“", - "ToastNewUserCreatedSuccess": "Създаден е нов акаунт", - "ToastNewUserLibraryError": "Трябва да изберете поне една библиотека", - "ToastNewUserPasswordError": "Трябва да има парола; само root потребителят може да бъде с празна парола", - "ToastNewUserTagError": "Трябва да изберете поне един етикет", - "ToastNewUserUsernameError": "Въведете потребителско име", - "ToastNoNewEpisodesFound": "Не са намерени нови епизоди", - "ToastNoRSSFeed": "Подкастът няма RSS емисия", - "ToastNoUpdatesNecessary": "Не са необходими актуализации", - "ToastNotificationCreateFailed": "Неуспешно създаване на известие", - "ToastNotificationDeleteFailed": "Неуспешно изтриване на известието", - "ToastNotificationFailedMaximum": "Максималният брой неуспешни опити трябва да бъде >= 0", - "ToastNotificationQueueMaximum": "Максималната опашка за известия трябва да бъде >= 0", - "ToastNotificationSettingsUpdateSuccess": "Настройките за известия са актуализирани", - "ToastNotificationTestTriggerFailed": "Неуспешно задействане на тестово известие", - "ToastNotificationTestTriggerSuccess": "Тестовото известие е задействано", - "ToastNotificationUpdateSuccess": "Известието е актуализирано", "ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист", "ToastPlaylistCreateSuccess": "Плейлистът е създаден", "ToastPlaylistRemoveSuccess": "Плейлистът е премахнат", "ToastPlaylistUpdateSuccess": "Плейлистът е обновен", "ToastPodcastCreateFailed": "Неуспешно създаване на подкаст", "ToastPodcastCreateSuccess": "Подкаст успешно създаден", - "ToastPodcastEpisodeUpdated": "Епизодът е актуализиран", - "ToastPodcastGetFeedFailed": "Неуспешно извличане на емисията на подкаста", - "ToastPodcastNoEpisodesInFeed": "Не са намерени епизоди в RSS емисията", - "ToastPodcastNoRssFeed": "Подкастът няма RSS емисия", - "ToastProgressIsNotBeingSynced": "Напредъкът не се синхронизира, рестартирайте възпроизвеждането", - "ToastProviderCreatedFailed": "Неуспешно добавяне на доставчик", - "ToastProviderCreatedSuccess": "Добавен е нов доставчик", - "ToastProviderNameAndUrlRequired": "Изискват се име и URL адрес", - "ToastProviderRemoveSuccess": "Доставчикът е премахнат", "ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията", "ToastRSSFeedCloseSuccess": "RSS емисията е затворена", - "ToastRemoveFailed": "Неуспешно премахване", "ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция", "ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция", - "ToastRemoveItemsWithIssuesFailed": "Неуспешно премахване на елементите от библиотеката с проблеми", - "ToastRemoveItemsWithIssuesSuccess": "Елементите от библиотеката с проблеми са премахнати", - "ToastRenameFailed": "Неуспешно преименуване", - "ToastRescanFailed": "Повторното сканиране е неуспешно за {0}", - "ToastRescanRemoved": "Повторното сканиране завърши: елементът е премахнат", - "ToastRescanUpToDate": "Повторното сканиране завърши: елементът вече е актуален", - "ToastRescanUpdated": "Повторното сканиране завърши: елементът е актуализиран", - "ToastScanFailed": "Неуспешно сканиране на елемент от библиотеката", - "ToastSelectAtLeastOneUser": "Изберете поне един потребител", "ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство", "ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"", - "ToastSeriesSubmitFailedSameName": "Не могат да бъдат добавени два сериала с едно и също име", "ToastSeriesUpdateFailed": "Неуспешно обновяване на серия", "ToastSeriesUpdateSuccess": "Серията е обновена", "ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани", - "ToastSessionCloseFailed": "Неуспешно затваряне на сесията", "ToastSessionDeleteFailed": "Неуспешно изтриване на сесия", "ToastSessionDeleteSuccess": "Сесията е изтрита", - "ToastSleepTimerDone": "Таймерът за заспиване приключи... zZzzZz", - "ToastSlugMustChange": "Краткият URL (slug) съдържа невалидни символи", - "ToastSlugRequired": "Изисква се кратък URL (slug)", "ToastSocketConnected": "Свързан сокет", "ToastSocketDisconnected": "Сокетът е прекъснат", "ToastSocketFailedToConnect": "Неуспешно свързване на сокет", "ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране", "ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)", - "ToastTitleRequired": "Изисква се заглавие", - "ToastUnknownError": "Неизвестна грешка", - "ToastUnlinkOpenIdFailed": "Неуспешно прекъсване на връзката на потребителя с OpenID", - "ToastUnlinkOpenIdSuccess": "Връзката на потребителя с OpenID е прекъсната", - "ToastUploaderFilepathExistsError": "Файловият път „{0}“ вече съществува на сървъра", - "ToastUploaderItemExistsInSubdirectoryError": "Елементът „{0}“ използва поддиректория на пътя за качване.", "ToastUserDeleteFailed": "Неуспешно изтриване на потребител", - "ToastUserDeleteSuccess": "Потребителят е изтрит", - "ToastUserPasswordChangeSuccess": "Паролата е променена успешно", - "ToastUserPasswordMismatch": "Паролите не съвпадат", - "ToastUserPasswordMustChange": "Новата парола не може да бъде същата като старата", - "ToastUserRootRequireName": "Трябва да въведете root потребителско име", - "TooltipAddChapters": "Добавяне на глава(и)", - "TooltipAddOneSecond": "Добавяне на 1 секунда", - "TooltipAdjustChapterStart": "Кликнете за коригиране на началния час", - "TooltipLockAllChapters": "Заключване на всички глави", - "TooltipLockChapter": "Заключване на глава (Shift+клик за диапазон)", - "TooltipSubtractOneSecond": "Изваждане на 1 секунда", - "TooltipUnlockAllChapters": "Отключване на всички глави", - "TooltipUnlockChapter": "Отключване на глава (Shift+клик за диапазон)" + "ToastUserDeleteSuccess": "Потребителят е изтрит" } diff --git a/client/strings/lv.json b/client/strings/lv.json deleted file mode 100644 index 0967ef424..000000000 --- a/client/strings/lv.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/client/strings/pl.json b/client/strings/pl.json index f04e61c46..d1bc6c062 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -951,11 +951,6 @@ "NoteUploaderFoldersWithMediaFiles": "Foldery z plikami multimedialnymi będą traktowane jako osobne elementy w bibliotece.", "NoteUploaderOnlyAudioFiles": "Jeśli przesyłasz tylko pliki audio, każdy plik audio będzie traktowany jako osobny audiobook.", "NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.", - "NotificationOnBackupCompletedDescription": "Wyzwalane po zakończeniu tworzenia kopii zapasowej", - "NotificationOnBackupFailedDescription": "Wyzwalane w przypadku gdy stworzenie kopii zapasowej rzuci błąd", - "NotificationOnEpisodeDownloadedDescription": "Wyzwalane, gdy odcinek podcastu zostanie automatycznie pobrany", - "NotificationOnRSSFeedDisabledDescription": "Wyzwalane, gdy automatyczne pobieranie odcinków jest wyłączone z powodu zbyt wielu nieudanych prób", - "NotificationOnRSSFeedFailedDescription": "Wyzwalane, gdy żądanie kanału RSS dotyczące automatycznego pobrania odcinka nie powiedzie się", "NotificationOnTestDescription": "Zdarzenie używane do testowania systemu powiadomień", "PlaceholderBulkChapterInput": "Wpisz tytuł rozdziału lub użyj numeracji (np. „Odcinek 1”, „Rozdział 10”, „1.”)", "PlaceholderNewCollection": "Nowa nazwa kolekcji", @@ -965,7 +960,6 @@ "PlaceholderSearchEpisode": "Szukanie odcinka..", "StatsAuthorsAdded": "dodano autorów", "StatsBooksAdded": "dodano książki", - "StatsBooksAdditional": "Niektóre dodatki obejmują…", "StatsBooksFinished": "ukończone książki", "StatsBooksFinishedThisYear": "Wybrane książki ukończone w tym roku…", "StatsBooksListenedTo": "książki wysłuchane", @@ -982,7 +976,6 @@ "StatsTotalDuration": "O sumarycznej długości…", "StatsYearInReview": "PRZEGLĄD ROKU", "ToastAccountUpdateSuccess": "Zaktualizowano konto", - "ToastAppriseUrlRequired": "Należy wprowadzić adres URL Apprise", "ToastAsinRequired": "ASIN jest wymagany", "ToastAuthorImageRemoveSuccess": "Zdjęcie autora usunięte", "ToastAuthorNotFound": "Autor \"{0}\" nie został znaleziony", @@ -1001,11 +994,8 @@ "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", - "ToastBatchApplyDetailsToItemsSuccess": "Szczegóły zastosowane do elementów", "ToastBatchDeleteFailed": "Usuwanie zbiorcze nie powiodło się", "ToastBatchDeleteSuccess": "Usuwanie zbiorcze powiodło się", - "ToastBatchQuickMatchFailed": "Szybkie dopasowanie partii nie powiodło się!", - "ToastBatchQuickMatchStarted": "Rozpoczęto partię szybkiego dopasowania {0} książek!", "ToastBatchUpdateFailed": "Aktualizacja zbiorcza nie powiodła się", "ToastBatchUpdateSuccess": "Aktualizacja zbiorcza powiodła się", "ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki", @@ -1043,14 +1033,7 @@ "ToastEpisodeDownloadQueueClearSuccess": "Wyczyszczono kolejkę epizodów do ściągnięcia", "ToastEpisodeUpdateSuccess": "Zaktualizowano {0} odcinków", "ToastErrorCannotShare": "Nie można udostępniać natywnie na tym urządzeniu.", - "ToastFailedToCreate": "Nie udało się utworzyć", - "ToastFailedToDelete": "Nie udało się usunąć", - "ToastFailedToLoadData": "Nie udało się załadować danych", - "ToastFailedToMatch": "Nie udało się dopasować", - "ToastFailedToShare": "Nie udało się udostępnić", - "ToastFailedToUpdate": "Nie udało się zaktualizować", "ToastInvalidImageUrl": "Nieprawidłowy URL obrazu", - "ToastInvalidMaxEpisodesToDownload": "Nieprawidłowa maksymalna liczba odcinków do pobrania", "ToastInvalidUrl": "Nieprawidłowy URL", "ToastInvalidUrls": "Jeden lub więcej URL-i są nieprawidłowe", "ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę", @@ -1061,7 +1044,6 @@ "ToastItemMarkedAsFinishedSuccess": "Pozycja oznaczona jako ukończona", "ToastItemMarkedAsNotFinishedFailed": "Oznaczenie pozycji jako ukończonej nie powiodło się", "ToastItemMarkedAsNotFinishedSuccess": "Pozycja oznaczona jako nieukończona", - "ToastItemUpdateSuccess": "Element zaktualizowany", "ToastLibraryCreateFailed": "Nie udało się utworzyć biblioteki", "ToastLibraryCreateSuccess": "Biblioteka \"{0}\" stworzona", "ToastLibraryDeleteFailed": "Nie udało się usunąć biblioteki", @@ -1070,10 +1052,6 @@ "ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki", "ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji", "ToastMatchAllAuthorsFailed": "Nie udało się dopasować wszystkich autorów", - "ToastMetadataFilesRemovedError": "Błąd podczas usuwania metadata.{0} plików", - "ToastMetadataFilesRemovedNoneFound": "Nie znaleziono metadata.{0} plików w bibliotece", - "ToastMetadataFilesRemovedNoneRemoved": "Nie usunięto żadnego metadata.{0} pliku", - "ToastMetadataFilesRemovedSuccess": "{0} metadata.{0} plików usunięto", "ToastMustHaveAtLeastOnePath": "Musi mieć przynajmniej jedną ścieżkę", "ToastNameEmailRequired": "Nazwa i email są wymagane", "ToastNameRequired": "Imię jest wymagane", @@ -1087,15 +1065,7 @@ "ToastNewUserUsernameError": "Wprowadź nazwę użytkownika", "ToastNoNewEpisodesFound": "Nie znaleziono nowych odcinków", "ToastNoRSSFeed": "Podcast nie posiada RSS Feed", - "ToastNoUpdatesNecessary": "Brak konieczności aktualizacji", - "ToastNotificationCreateFailed": "Nie udało się utworzyć powiadomienia", - "ToastNotificationDeleteFailed": "Nie udało się usunąć powiadomienia", "ToastNotificationFailedMaximum": "Maks. ilość nieudanych prób musi być >= 0", - "ToastNotificationQueueMaximum": "Maksymalna liczba powiadomień w kolejce musi być >= 0", - "ToastNotificationSettingsUpdateSuccess": "Zaktualizowano ustawienia powiadomień", - "ToastNotificationTestTriggerFailed": "Nie udało się wywołać powiadomienia testowego", - "ToastNotificationTestTriggerSuccess": "Wyzwolono powiadomienie testowe", - "ToastNotificationUpdateSuccess": "Powiadomienie zaktualizowane", "ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty", "ToastPlaylistCreateSuccess": "Playlista utworzona", "ToastPlaylistRemoveSuccess": "Playlista usunięta", @@ -1103,17 +1073,8 @@ "ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu", "ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony", "ToastPodcastEpisodeUpdated": "Zaktualizowano odcinki", - "ToastPodcastGetFeedFailed": "Nie udało się pobrać kanału podcastu", - "ToastPodcastNoEpisodesInFeed": "Nie znaleziono żadnych odcinków w kanale RSS", - "ToastPodcastNoRssFeed": "Podcast nie ma kanału RSS", - "ToastProgressIsNotBeingSynced": "Postęp nie jest synchronizowany, uruchom ponownie odtwarzanie", - "ToastProviderCreatedFailed": "Nie udało się dodać dostawcy", - "ToastProviderCreatedSuccess": "Dodano nowego dostawcę", - "ToastProviderNameAndUrlRequired": "Wymagane jest podanie nazwy i adresu URL", - "ToastProviderRemoveSuccess": "Dostawca usunięty", "ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się", "ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się", - "ToastRemoveFailed": "Nie udało się usunąć", "ToastRemoveItemFromCollectionFailed": "Nie udało się usunąć elementu z kolekcji", "ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji", "ToastRemoveItemsWithIssuesFailed": "Nie udało się usunąć wadliwych elementów z biblioteki", @@ -1135,25 +1096,16 @@ "ToastSessionDeleteFailed": "Nie udało się usunąć sesji", "ToastSessionDeleteSuccess": "Sesja usunięta", "ToastSleepTimerDone": "Słodkich snów... zZzzZz", - "ToastSlugMustChange": "Slug zawiera nieprawidłowe znaki", - "ToastSlugRequired": "Slug jest wymagany", "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ę", - "ToastSortingPrefixesEmptyError": "Musi mieć co najmniej 1 prefiks sortowania", - "ToastSortingPrefixesUpdateSuccess": "Zaktualizowano prefiksy sortowania ({0} elementów)", "ToastTitleRequired": "Tytuł jest wymagany", "ToastUnknownError": "Nieznany błąd", "ToastUnlinkOpenIdFailed": "Nie udało się odpiąć użytkownika z OpenID", "ToastUnlinkOpenIdSuccess": "Użytkownik odpięty z OpenID", "ToastUploaderFilepathExistsError": "Ścieżka \"{0}\" już istnieje na serwerze", - "ToastUploaderItemExistsInSubdirectoryError": "Element \"{0}\" używa podkatalogu ścieżki przesyłania.", "ToastUserDeleteFailed": "Nie udało się usunąć użytkownika", "ToastUserDeleteSuccess": "Użytkownik usunięty", - "ToastUserPasswordChangeSuccess": "Hasło zostało pomyślnie zmienione", - "ToastUserPasswordMismatch": "Hasła nie są zgodne", - "ToastUserPasswordMustChange": "Nowe hasło nie może być takie samo jak stare hasło", - "ToastUserRootRequireName": "Należy wprowadzić nazwę użytkownika root", "TooltipAddChapters": "Dodaj rozdział(y)", "TooltipAddOneSecond": "Dodaj sekundę", "TooltipAdjustChapterStart": "Kliknij, aby skorygować czas początkowy", diff --git a/package-lock.json b/package-lock.json index 312675d71..8950b4903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.34.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.34.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index a0b340244..10ba26f65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.35.0", + "version": "2.34.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index a54231d75..ad55f6605 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -3,7 +3,6 @@ const Logger = require('./Logger') const Database = require('./Database') const TokenManager = require('./auth/TokenManager') const CoverSearchManager = require('./managers/CoverSearchManager') -const { LogLevel } = require('./utils/constants') /** * @typedef SocketClient @@ -86,14 +85,6 @@ class SocketAuthority { } } - requireAdminSocket(socket, eventName) { - const client = this.clients[socket.id] - if (client?.user?.isAdminOrUp) return true - - Logger.warn(`[SocketAuthority] Unauthorized ${eventName} socket event from socket ${socket.id}`) - return false - } - /** * Emits event with library item to all clients that can access the library item * Note: Emits toOldJSONExpanded() @@ -188,25 +179,14 @@ class SocketAuthority { socket.on('auth', (token) => this.authenticateSocket(socket, token)) // Scanning - socket.on('cancel_scan', (libraryId) => { - if (!this.requireAdminSocket(socket, 'cancel_scan')) return - this.cancelScan(libraryId) - }) + socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId)) // Cover search streaming socket.on('search_covers', (payload) => this.handleCoverSearch(socket, payload)) socket.on('cancel_cover_search', (requestId) => this.handleCancelCoverSearch(socket, requestId)) // Logs - socket.on('set_log_listener', (level) => { - if (!this.requireAdminSocket(socket, 'set_log_listener')) return - - if (!Number.isInteger(level) || !Object.values(LogLevel).includes(level)) { - Logger.warn(`[SocketAuthority] Invalid set_log_listener level from socket ${socket.id}`) - return - } - Logger.addSocketListener(socket, level) - }) + socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) // Sent automatically from socket.io clients diff --git a/server/auth/TokenManager.js b/server/auth/TokenManager.js index 5933209c7..5efeb7a64 100644 --- a/server/auth/TokenManager.js +++ b/server/auth/TokenManager.js @@ -1,5 +1,4 @@ const { Op } = require('sequelize') -const uuid = require('uuid') const Database = require('../Database') const Logger = require('../Logger') @@ -116,7 +115,6 @@ class TokenManager { const payload = { userId: user.id, username: user.username, - jti: uuid.v4(), type: 'access' } const options = { @@ -140,7 +138,6 @@ class TokenManager { const payload = { userId: user.id, username: user.username, - jti: uuid.v4(), type: 'refresh' } const options = { @@ -186,56 +183,20 @@ class TokenManager { * @param {import('../models/User')} user * @param {import('express').Request} req * @param {import('express').Response} res - * @param {boolean} gracePeriod - whether to use the grace period * @returns {Promise<{ accessToken:string, refreshToken:string }>} */ - async rotateTokensForSession(session, user, req, res, gracePeriod = true) { - const previousRefreshToken = session.refreshToken + async rotateTokensForSession(session, user, req, res) { + // Generate new tokens const newAccessToken = this.generateTempAccessToken(user) - let newRefreshToken = this.generateRefreshToken(user) + const newRefreshToken = this.generateRefreshToken(user) + + // Calculate new expiration time const newExpiresAt = new Date(Date.now() + this.RefreshTokenExpiry * 1000) - let lastRefreshToken = null - let lastRefreshTokenExpiresAt = null - if (gracePeriod) { - // Set grace period of old refresh token in case of race condition in token rotation. - // This grace period may need to be longer if fetching the user data takes longer due to large progress objects - lastRefreshToken = previousRefreshToken - lastRefreshTokenExpiresAt = new Date(Date.now() + 60 * 1000) // 1 minute grace period - } - - // Only update if this session row still has the refresh token we read - const [numUpdated] = await Database.sessionModel.update( - { - refreshToken: newRefreshToken, - expiresAt: newExpiresAt, - lastRefreshToken, - lastRefreshTokenExpiresAt - }, - { - where: { - id: session.id, - refreshToken: previousRefreshToken - } - } - ) - - if (numUpdated === 0) { - Logger.debug(`[TokenManager] Race condition in rotateTokensForSession for user ${user.id}, getting new token`) - - const updatedSession = await Database.sessionModel.findOne({ where: { id: session.id } }) - - newRefreshToken = updatedSession.refreshToken - session.refreshToken = updatedSession.refreshToken - session.expiresAt = updatedSession.expiresAt - session.lastRefreshToken = updatedSession.lastRefreshToken - session.lastRefreshTokenExpiresAt = updatedSession.lastRefreshTokenExpiresAt - } else { - session.refreshToken = newRefreshToken - session.expiresAt = newExpiresAt - session.lastRefreshToken = lastRefreshToken - session.lastRefreshTokenExpiresAt = lastRefreshTokenExpiresAt - } + // Update the session with the new refresh token and expiration + session.refreshToken = newRefreshToken + session.expiresAt = newExpiresAt + await session.save() // Set new refresh token cookie this.setRefreshTokenCookie(req, res, newRefreshToken) @@ -333,40 +294,23 @@ class TokenManager { } } - let session = await Database.sessionModel.findOne({ - where: { - [Op.or]: [{ refreshToken: refreshToken }, { lastRefreshToken: refreshToken }] - } + const session = await Database.sessionModel.findOne({ + where: { refreshToken: refreshToken } }) if (!session) { - Logger.error(`[TokenManager] Failed to refresh token. Session not found`) + Logger.error(`[TokenManager] Failed to refresh token. Session not found for refresh token: ${refreshToken}`) return { error: 'Invalid refresh token' } } - let isGracePeriod = false - if (session.refreshToken !== refreshToken) { - // Token matched lastRefreshToken - if (session.lastRefreshTokenExpiresAt && session.lastRefreshTokenExpiresAt > new Date()) { - isGracePeriod = true - Logger.debug(`[TokenManager] Grace period hit for user ${session.userId}`) - } else { - Logger.debug(`[TokenManager] Grace period expired for user ${session.userId}`) - return { - error: 'Invalid refresh token' - } - } - } else { - // Token matched current refreshToken - // Check if session is expired in database - if (session.expiresAt < new Date()) { - Logger.info(`[TokenManager] Session expired in database, cleaning up`) - await session.destroy() - return { - error: 'Refresh token expired' - } + // Check if session is expired in database + if (session.expiresAt < new Date()) { + Logger.info(`[TokenManager] Session expired in database, cleaning up`) + await session.destroy() + return { + error: 'Refresh token expired' } } @@ -378,20 +322,6 @@ class TokenManager { } } - if (isGracePeriod) { - // Return the already rotated refresh token store in the database, - // and generate a new access token without changing the refresh token - // again - const accessToken = this.generateTempAccessToken(user) - this.setRefreshTokenCookie(req, res, session.refreshToken) - - return { - accessToken, - refreshToken: session.refreshToken, - user - } - } - const newTokens = await this.rotateTokensForSession(session, user, req, res) return { accessToken: newTokens.accessToken, @@ -445,7 +375,7 @@ class TokenManager { // So rotate token for current session const currentSession = await Database.sessionModel.findOne({ where: { refreshToken: currentRefreshToken } }) if (currentSession) { - const newTokens = await this.rotateTokensForSession(currentSession, user, req, res, false) + const newTokens = await this.rotateTokensForSession(currentSession, user, req, res) // Invalidate all sessions for the user except the current one await Database.sessionModel.destroy({ @@ -459,7 +389,7 @@ class TokenManager { return newTokens.accessToken } else { - Logger.error(`[TokenManager] No session found to rotate tokens`) + Logger.error(`[TokenManager] No session found to rotate tokens for refresh token ${currentRefreshToken}`) } } @@ -483,7 +413,7 @@ class TokenManager { try { const numDeleted = await Database.sessionModel.destroy({ where: { refreshToken: refreshToken } }) - Logger.info(`[TokenManager] Refresh token invalidated, ${numDeleted} sessions deleted`) + Logger.info(`[TokenManager] Refresh token ${refreshToken} invalidated, ${numDeleted} sessions deleted`) return true } catch (error) { Logger.error(`[TokenManager] Error invalidating refresh token: ${error.message}`) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 8c2e80aec..80471ec47 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -149,7 +149,7 @@ class AuthorController { }) if (libraryItems.length) { await Database.bookAuthorModel.removeByIds(req.author.id) // Remove all old BookAuthor - await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate, { ignoreDuplicates: true }) // Create all new unique BookAuthor + await Database.bookAuthorModel.bulkCreate(bookAuthorsToCreate) // Create all new BookAuthor for (const libraryItem of libraryItems) { await libraryItem.saveMetadataFile() } diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index a066a0d32..c4681bdc2 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -2,7 +2,6 @@ const { Request, Response } = require('express') const Path = require('path') const Logger = require('../Logger') -const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') @@ -217,11 +216,6 @@ class RssFeedManager { res.sendStatus(404) return } - // Express does not set the correct mimetype for m4b files so use our defined mimetypes if available - const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(episodePath)) - if (audioMimeType) { - res.setHeader('Content-Type', audioMimeType) - } res.sendFile(episodePath) } diff --git a/server/migrations/v2.35.0-add-last-refresh-token.js b/server/migrations/v2.35.0-add-last-refresh-token.js deleted file mode 100644 index 0ad190e9a..000000000 --- a/server/migrations/v2.35.0-add-last-refresh-token.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @typedef MigrationContext - * @property {import('sequelize').QueryInterface} queryInterface - a Sequelize QueryInterface object. - * @property {import('../Logger')} logger - a Logger object. - * - * @typedef MigrationOptions - * @property {MigrationContext} context - an object containing the migration context. - */ - -const migrationVersion = '2.35.0' -const migrationName = `${migrationVersion}-add-last-refresh-token` -const loggerPrefix = `[${migrationVersion} migration]` - -/** - * This migration script adds lastRefreshToken and lastRefreshTokenExpiresAt columns to the sessions table. - * - * @param {MigrationOptions} options - an object containing the migration context. - * @returns {Promise} - A promise that resolves when the migration is complete. - */ -async function up({ context: { queryInterface, logger } }) { - logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`) - - if (await queryInterface.tableExists('sessions')) { - const tableDescription = await queryInterface.describeTable('sessions') - - if (!tableDescription.lastRefreshToken) { - logger.info(`${loggerPrefix} Adding lastRefreshToken column to sessions table`) - await queryInterface.addColumn('sessions', 'lastRefreshToken', { - type: queryInterface.sequelize.Sequelize.DataTypes.STRING, - allowNull: true - }) - } else { - logger.info(`${loggerPrefix} lastRefreshToken column already exists in sessions table`) - } - - if (!tableDescription.lastRefreshTokenExpiresAt) { - logger.info(`${loggerPrefix} Adding lastRefreshTokenExpiresAt column to sessions table`) - await queryInterface.addColumn('sessions', 'lastRefreshTokenExpiresAt', { - type: queryInterface.sequelize.Sequelize.DataTypes.DATE, - allowNull: true - }) - } else { - logger.info(`${loggerPrefix} lastRefreshTokenExpiresAt column already exists in sessions table`) - } - } else { - logger.info(`${loggerPrefix} sessions table does not exist`) - } - - logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`) -} - -/** - * This migration script removes the lastRefreshToken and lastRefreshTokenExpiresAt columns from the sessions table. - * - * @param {MigrationOptions} options - an object containing the migration context. - * @returns {Promise} - A promise that resolves when the migration is complete. - */ -async function down({ context: { queryInterface, logger } }) { - logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`) - - if (await queryInterface.tableExists('sessions')) { - const tableDescription = await queryInterface.describeTable('sessions') - - if (tableDescription.lastRefreshToken) { - logger.info(`${loggerPrefix} Removing lastRefreshToken column from sessions table`) - await queryInterface.removeColumn('sessions', 'lastRefreshToken') - } else { - logger.info(`${loggerPrefix} lastRefreshToken column does not exist in sessions table`) - } - - if (tableDescription.lastRefreshTokenExpiresAt) { - logger.info(`${loggerPrefix} Removing lastRefreshTokenExpiresAt column from sessions table`) - await queryInterface.removeColumn('sessions', 'lastRefreshTokenExpiresAt') - } else { - logger.info(`${loggerPrefix} lastRefreshTokenExpiresAt column does not exist in sessions table`) - } - } else { - logger.info(`${loggerPrefix} sessions table does not exist`) - } - - logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`) -} - -module.exports = { up, down } diff --git a/server/models/Session.js b/server/models/Session.js index 3b85bd46a..fe9dd5425 100644 --- a/server/models/Session.js +++ b/server/models/Session.js @@ -18,10 +18,6 @@ class Session extends Model { this.userId /** @type {Date} */ this.expiresAt - /** @type {string} */ - this.lastRefreshToken - /** @type {Date} */ - this.lastRefreshTokenExpiresAt // Expanded properties @@ -70,14 +66,6 @@ class Session extends Model { expiresAt: { type: DataTypes.DATE, allowNull: false - }, - lastRefreshToken: { - type: DataTypes.STRING, - allowNull: true - }, - lastRefreshTokenExpiresAt: { - type: DataTypes.DATE, - allowNull: true } }, { diff --git a/server/objects/DeviceInfo.js b/server/objects/DeviceInfo.js index 68237f0fb..22ebfbea4 100644 --- a/server/objects/DeviceInfo.js +++ b/server/objects/DeviceInfo.js @@ -96,12 +96,7 @@ class DeviceInfo { this.clientVersion = stripAllTags(clientDeviceInfo?.clientVersion) || serverVersion this.manufacturer = stripAllTags(clientDeviceInfo?.manufacturer) || null this.model = stripAllTags(clientDeviceInfo?.model) || null - - if (typeof clientDeviceInfo?.sdkVersion === 'number') { - this.sdkVersion = clientDeviceInfo.sdkVersion.toString() - } else { - this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null - } + this.sdkVersion = stripAllTags(clientDeviceInfo?.sdkVersion) || null this.clientName = stripAllTags(clientDeviceInfo?.clientName) || null if (this.sdkVersion) { diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index ac93c6379..a1e7ff507 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -7,7 +7,6 @@ const parseNameString = require('../utils/parsers/parseNameString') const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') const globals = require('../utils/globals') const { readTextFile, filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') -const htmlSanitizer = require('../utils/htmlSanitizer') const AudioFileScanner = require('./AudioFileScanner') const Database = require('../Database') @@ -689,10 +688,6 @@ class BookScanner { bookMetadata.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title) - if (typeof bookMetadata.description === 'string' && bookMetadata.description) { - bookMetadata.description = htmlSanitizer.sanitize(bookMetadata.description) - } - return bookMetadata } diff --git a/server/scanner/PodcastScanner.js b/server/scanner/PodcastScanner.js index 6ab2d332c..c9569c3ad 100644 --- a/server/scanner/PodcastScanner.js +++ b/server/scanner/PodcastScanner.js @@ -11,7 +11,6 @@ const LibraryFile = require('../objects/files/LibraryFile') const fsExtra = require('../libs/fsExtra') const PodcastEpisode = require('../models/PodcastEpisode') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') -const htmlSanitizer = require('../utils/htmlSanitizer') /** * Metadata for podcasts pulled from files @@ -399,10 +398,6 @@ class PodcastScanner { podcastMetadata.titleIgnorePrefix = getTitleIgnorePrefix(podcastMetadata.title) - if (typeof podcastMetadata.description === 'string' && podcastMetadata.description) { - podcastMetadata.description = htmlSanitizer.sanitize(podcastMetadata.description) - } - return podcastMetadata }