Merge branch 'master' into mf/rssInboundManager

This commit is contained in:
mfcar 2023-12-17 00:17:13 -03:00
commit e9a1317306
No known key found for this signature in database
12 changed files with 198 additions and 114 deletions

View file

@ -332,6 +332,7 @@ export default {
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}` if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}` var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}` if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}`
if (this.libraryItemId) searchQuery += `&id=${this.libraryItemId}`
return searchQuery return searchQuery
}, },
submitSearch() { submitSearch() {

View file

@ -1,10 +1,10 @@
{ {
"ButtonAdd": "Hinzufügen", "ButtonAdd": "Hinzufügen",
"ButtonAddChapters": "Kapitel hinzufügen", "ButtonAddChapters": "Kapitel hinzufügen",
"ButtonAddDevice": "Add Device", "ButtonAddDevice": "Gerät hinzufügen",
"ButtonAddLibrary": "Add Library", "ButtonAddLibrary": "Bibliothek hinzufügen",
"ButtonAddPodcasts": "Podcasts hinzufügen", "ButtonAddPodcasts": "Podcasts hinzufügen",
"ButtonAddUser": "Add User", "ButtonAddUser": "Benutzer hinzufügen",
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek", "ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
"ButtonApply": "Übernehmen", "ButtonApply": "Übernehmen",
"ButtonApplyChapters": "Kapitel anwenden", "ButtonApplyChapters": "Kapitel anwenden",
@ -58,11 +58,11 @@
"ButtonRemoveAll": "Alles löschen", "ButtonRemoveAll": "Alles löschen",
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge", "ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste", "ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
"ButtonRemoveFromContinueReading": "Remove from Continue Reading", "ButtonRemoveFromContinueReading": "Lösche die Serie aus der Lesefortsetzungsliste",
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste", "ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
"ButtonReScan": "Neu scannen", "ButtonReScan": "Neu scannen",
"ButtonReset": "Zurücksetzen", "ButtonReset": "Zurücksetzen",
"ButtonResetToDefault": "Reset to default", "ButtonResetToDefault": "Zurücksetzen auf Standard",
"ButtonRestore": "Wiederherstellen", "ButtonRestore": "Wiederherstellen",
"ButtonSave": "Speichern", "ButtonSave": "Speichern",
"ButtonSaveAndClose": "Speichern & Schließen", "ButtonSaveAndClose": "Speichern & Schließen",
@ -221,7 +221,7 @@
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelChaptersFound": "gefundene Kapitel", "LabelChaptersFound": "gefundene Kapitel",
"LabelChapterTitle": "Kapitelüberschrift", "LabelChapterTitle": "Kapitelüberschrift",
"LabelClickForMoreInfo": "Click for more info", "LabelClickForMoreInfo": "Klicken für mehr Informationen",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Serien zusammenfassen", "LabelCollapseSeries": "Serien zusammenfassen",
@ -251,7 +251,7 @@
"LabelDiscFromMetadata": "CD aus den Metadaten", "LabelDiscFromMetadata": "CD aus den Metadaten",
"LabelDiscover": "Entdecken", "LabelDiscover": "Entdecken",
"LabelDownload": "Herunterladen", "LabelDownload": "Herunterladen",
"LabelDownloadNEpisodes": "Download {0} episodes", "LabelDownloadNEpisodes": "Download {0} Episoden",
"LabelDuration": "Laufzeit", "LabelDuration": "Laufzeit",
"LabelDurationFound": "Gefundene Laufzeit:", "LabelDurationFound": "Gefundene Laufzeit:",
"LabelEbook": "E-Book", "LabelEbook": "E-Book",

View file

@ -1,10 +1,10 @@
{ {
"ButtonAdd": "Добавить", "ButtonAdd": "Добавить",
"ButtonAddChapters": "Добавить главы", "ButtonAddChapters": "Добавить главы",
"ButtonAddDevice": "Add Device", "ButtonAddDevice": "Добавить устройство",
"ButtonAddLibrary": "Add Library", "ButtonAddLibrary": "Добавить библиотеку",
"ButtonAddPodcasts": "Добавить подкасты", "ButtonAddPodcasts": "Добавить подкасты",
"ButtonAddUser": "Add User", "ButtonAddUser": "Добавить пользователя",
"ButtonAddYourFirstLibrary": "Добавьте Вашу первую библиотеку", "ButtonAddYourFirstLibrary": "Добавьте Вашу первую библиотеку",
"ButtonApply": "Применить", "ButtonApply": "Применить",
"ButtonApplyChapters": "Применить главы", "ButtonApplyChapters": "Применить главы",
@ -62,7 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию", "ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
"ButtonReScan": "Пересканировать", "ButtonReScan": "Пересканировать",
"ButtonReset": "Сбросить", "ButtonReset": "Сбросить",
"ButtonResetToDefault": "Reset to default", "ButtonResetToDefault": "Сборосить по умолчанию",
"ButtonRestore": "Восстановить", "ButtonRestore": "Восстановить",
"ButtonSave": "Сохранить", "ButtonSave": "Сохранить",
"ButtonSaveAndClose": "Сохранить и закрыть", "ButtonSaveAndClose": "Сохранить и закрыть",
@ -78,7 +78,7 @@
"ButtonStartM4BEncode": "Начать кодирование M4B", "ButtonStartM4BEncode": "Начать кодирование M4B",
"ButtonStartMetadataEmbed": "Начать встраивание метаданных", "ButtonStartMetadataEmbed": "Начать встраивание метаданных",
"ButtonSubmit": "Применить", "ButtonSubmit": "Применить",
"ButtonTest": "Test", "ButtonTest": "Тест",
"ButtonUpload": "Загрузить", "ButtonUpload": "Загрузить",
"ButtonUploadBackup": "Загрузить бэкап", "ButtonUploadBackup": "Загрузить бэкап",
"ButtonUploadCover": "Загрузить обложку", "ButtonUploadCover": "Загрузить обложку",
@ -87,15 +87,15 @@
"ButtonUserEdit": "Редактировать пользователя {0}", "ButtonUserEdit": "Редактировать пользователя {0}",
"ButtonViewAll": "Посмотреть все", "ButtonViewAll": "Посмотреть все",
"ButtonYes": "Да", "ButtonYes": "Да",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata", "ErrorUploadFetchMetadataAPI": "Ошибка при получении метаданных",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", "ErrorUploadFetchMetadataNoResults": "Не удалось получить метаданные - попробуйте обновить название и/или автора",
"ErrorUploadLacksTitle": "Must have a title", "ErrorUploadLacksTitle": "Название должно быть заполнено",
"HeaderAccount": "Учетная запись", "HeaderAccount": "Учетная запись",
"HeaderAdvanced": "Дополнительно", "HeaderAdvanced": "Дополнительно",
"HeaderAppriseNotificationSettings": "Настройки оповещений", "HeaderAppriseNotificationSettings": "Настройки оповещений",
"HeaderAudiobookTools": "Инструменты файлов аудиокниг", "HeaderAudiobookTools": "Инструменты файлов аудиокниг",
"HeaderAudioTracks": "Аудио треки", "HeaderAudioTracks": "Аудио треки",
"HeaderAuthentication": "Authentication", "HeaderAuthentication": "Аутентификация",
"HeaderBackups": "Бэкапы", "HeaderBackups": "Бэкапы",
"HeaderChangePassword": "Изменить пароль", "HeaderChangePassword": "Изменить пароль",
"HeaderChapters": "Главы", "HeaderChapters": "Главы",
@ -130,15 +130,15 @@
"HeaderManageTags": "Редактировать теги", "HeaderManageTags": "Редактировать теги",
"HeaderMapDetails": "Найти подробности", "HeaderMapDetails": "Найти подробности",
"HeaderMatch": "Поиск", "HeaderMatch": "Поиск",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataOrderOfPrecedence": "Порядок приоритета метаданных",
"HeaderMetadataToEmbed": "Метаинформация для встраивания", "HeaderMetadataToEmbed": "Метаинформация для встраивания",
"HeaderNewAccount": "Новая учетная запись", "HeaderNewAccount": "Новая учетная запись",
"HeaderNewLibrary": "Новая библиотека", "HeaderNewLibrary": "Новая библиотека",
"HeaderNotifications": "Уведомления", "HeaderNotifications": "Уведомления",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenIDConnectAuthentication": "Аутентификация OpenID Connect",
"HeaderOpenRSSFeed": "Открыть RSS-канал", "HeaderOpenRSSFeed": "Открыть RSS-канал",
"HeaderOtherFiles": "Другие файлы", "HeaderOtherFiles": "Другие файлы",
"HeaderPasswordAuthentication": "Password Authentication", "HeaderPasswordAuthentication": "Аутентификация по паролю",
"HeaderPermissions": "Разрешения", "HeaderPermissions": "Разрешения",
"HeaderPlayerQueue": "Очередь воспроизведения", "HeaderPlayerQueue": "Очередь воспроизведения",
"HeaderPlaylist": "Плейлист", "HeaderPlaylist": "Плейлист",
@ -187,11 +187,11 @@
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию", "LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
"LabelAddToPlaylist": "Добавить в плейлист", "LabelAddToPlaylist": "Добавить в плейлист",
"LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист", "LabelAddToPlaylistBatch": "Добавить {0} элементов в плейлист",
"LabelAdminUsersOnly": "Admin users only", "LabelAdminUsersOnly": "Только для пользователей с правами администратора",
"LabelAll": "Все", "LabelAll": "Все",
"LabelAllUsers": "Все пользователи", "LabelAllUsers": "Все пользователи",
"LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersExcludingGuests": "Все пользователи, кроме гостей",
"LabelAllUsersIncludingGuests": "All users including guests", "LabelAllUsersIncludingGuests": "Все пользователи, включая гостей",
"LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке", "LabelAlreadyInYourLibrary": "Уже в Вашей библиотеке",
"LabelAppend": "Добавить", "LabelAppend": "Добавить",
"LabelAuthor": "Автор", "LabelAuthor": "Автор",
@ -199,14 +199,14 @@
"LabelAuthorLastFirst": "Автор (Фамилия, Имя)", "LabelAuthorLastFirst": "Автор (Фамилия, Имя)",
"LabelAuthors": "Авторы", "LabelAuthors": "Авторы",
"LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически", "LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически",
"LabelAutoFetchMetadata": "Auto Fetch Metadata", "LabelAutoFetchMetadata": "Автоматическое извлечение метаданных",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.", "LabelAutoFetchMetadataHelp": "Извлекает метаданные для названия, автора и серии для упрощения загрузки. После загрузки может потребоваться сопоставление дополнительных метаданных.",
"LabelAutoLaunch": "Auto Launch", "LabelAutoLaunch": "Автозапуск",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", "LabelAutoLaunchDescription": "Редирект на провайдера аутентификации автоматически при переходе на страницу входа (путь ручного переопределения <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register", "LabelAutoRegister": "Автоматическая регистрация",
"LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelAutoRegisterDescription": "Автоматическое создание новых пользователей после входа в систему",
"LabelBackToUser": "Назад к пользователю", "LabelBackToUser": "Назад к пользователю",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Путь для бэкапов",
"LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование", "LabelBackupsEnableAutomaticBackups": "Включить автоматическое бэкапирование",
"LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Бэкапы сохраняются в /metadata/backups",
"LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB)", "LabelBackupsMaxBackupSize": "Максимальный размер бэкапа (в GB)",
@ -215,13 +215,13 @@
"LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.", "LabelBackupsNumberToKeepHelp": "За один раз только 1 бэкап будет удален, так что если у вас будет больше бэкапов, то их нужно удалить вручную.",
"LabelBitrate": "Битрейт", "LabelBitrate": "Битрейт",
"LabelBooks": "Книги", "LabelBooks": "Книги",
"LabelButtonText": "Button Text", "LabelButtonText": "Текст кнопки",
"LabelChangePassword": "Изменить пароль", "LabelChangePassword": "Изменить пароль",
"LabelChannels": "Каналы", "LabelChannels": "Каналы",
"LabelChapters": "Главы", "LabelChapters": "Главы",
"LabelChaptersFound": "глав найдено", "LabelChaptersFound": "глав найдено",
"LabelChapterTitle": "Название главы", "LabelChapterTitle": "Название главы",
"LabelClickForMoreInfo": "Click for more info", "LabelClickForMoreInfo": "Нажмите, чтобы узнать больше",
"LabelClosePlayer": "Закрыть проигрыватель", "LabelClosePlayer": "Закрыть проигрыватель",
"LabelCodec": "Кодек", "LabelCodec": "Кодек",
"LabelCollapseSeries": "Свернуть серии", "LabelCollapseSeries": "Свернуть серии",
@ -240,12 +240,12 @@
"LabelCurrently": "Текущее:", "LabelCurrently": "Текущее:",
"LabelCustomCronExpression": "Пользовательское выражение Cron:", "LabelCustomCronExpression": "Пользовательское выражение Cron:",
"LabelDatetime": "Дата и время", "LabelDatetime": "Дата и время",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDeleteFromFileSystemCheckbox": "Удалить из файловой системы (снимите флажок, чтобы удалить только из базы данных)",
"LabelDescription": "Описание", "LabelDescription": "Описание",
"LabelDeselectAll": "Снять выделение", "LabelDeselectAll": "Снять выделение",
"LabelDevice": "Устройство", "LabelDevice": "Устройство",
"LabelDeviceInfo": "Информация об устройстве", "LabelDeviceInfo": "Информация об устройстве",
"LabelDeviceIsAvailableTo": "Device is available to...", "LabelDeviceIsAvailableTo": "Устройство доступно для...",
"LabelDirectory": "Каталог", "LabelDirectory": "Каталог",
"LabelDiscFromFilename": "Диск из Имени файла", "LabelDiscFromFilename": "Диск из Имени файла",
"LabelDiscFromMetadata": "Диск из Метаданных", "LabelDiscFromMetadata": "Диск из Метаданных",
@ -271,7 +271,7 @@
"LabelExample": "Пример", "LabelExample": "Пример",
"LabelExplicit": "Явный", "LabelExplicit": "Явный",
"LabelFeedURL": "URL канала", "LabelFeedURL": "URL канала",
"LabelFetchingMetadata": "Fetching Metadata", "LabelFetchingMetadata": "Извлечение метаданных",
"LabelFile": "Файл", "LabelFile": "Файл",
"LabelFileBirthtime": "Дата создания", "LabelFileBirthtime": "Дата создания",
"LabelFileModified": "Дата модификации", "LabelFileModified": "Дата модификации",
@ -289,11 +289,11 @@
"LabelHardDeleteFile": "Жесткое удаление файла", "LabelHardDeleteFile": "Жесткое удаление файла",
"LabelHasEbook": "Есть e-книга", "LabelHasEbook": "Есть e-книга",
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
"LabelHighestPriority": "Highest priority", "LabelHighestPriority": "Наивысший приоритет",
"LabelHost": "Хост", "LabelHost": "Хост",
"LabelHour": "Часы", "LabelHour": "Часы",
"LabelIcon": "Иконка", "LabelIcon": "Иконка",
"LabelImageURLFromTheWeb": "Image URL from the web", "LabelImageURLFromTheWeb": "URL-адрес изображения из Интернета",
"LabelIncludeInTracklist": "Включать в список воспроизведения", "LabelIncludeInTracklist": "Включать в список воспроизведения",
"LabelIncomplete": "Не завершен", "LabelIncomplete": "Не завершен",
"LabelInProgress": "В процессе", "LabelInProgress": "В процессе",
@ -331,20 +331,20 @@
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты",
"LabelLowestPriority": "Lowest Priority", "LabelLowestPriority": "Самый низкий приоритет",
"LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersBy": "Сопоставление существующих пользователей по",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMatchExistingUsersByDescription": "Используется для подключения существующих пользователей. После подключения пользователям будет присвоен уникальный идентификатор от поставщика единого входа",
"LabelMediaPlayer": "Медиа проигрыватель", "LabelMediaPlayer": "Медиа проигрыватель",
"LabelMediaType": "Тип медиа", "LabelMediaType": "Тип медиа",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataOrderOfPrecedenceDescription": "Источники метаданных с более высоким приоритетом будут переопределять источники метаданных с более низким приоритетом",
"LabelMetadataProvider": "Провайдер", "LabelMetadataProvider": "Провайдер",
"LabelMetaTag": "Мета тег", "LabelMetaTag": "Мета тег",
"LabelMetaTags": "Мета теги", "LabelMetaTags": "Мета теги",
"LabelMinute": "Минуты", "LabelMinute": "Минуты",
"LabelMissing": "Потеряно", "LabelMissing": "Потеряно",
"LabelMissingParts": "Потерянные части", "LabelMissingParts": "Потерянные части",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "Это белый список допустимых URI перенаправления для мобильных приложений. По умолчанию используется <code>audiobookshelf://oauth</code>, который можно удалить или дополнить дополнительными URI для интеграции со сторонними приложениями. Использование звездочки (<code>*</code>) в качестве единственной записи разрешает любой URI.",
"LabelMore": "Еще", "LabelMore": "Еще",
"LabelMoreInfo": "Больше информации", "LabelMoreInfo": "Больше информации",
"LabelName": "Имя", "LabelName": "Имя",
@ -523,7 +523,7 @@
"LabelUpdateDetailsHelp": "Позволяет перезаписывать текущие подробности для выбранных книг если будут найдены", "LabelUpdateDetailsHelp": "Позволяет перезаписывать текущие подробности для выбранных книг если будут найдены",
"LabelUploaderDragAndDrop": "Перетащите файлы или каталоги", "LabelUploaderDragAndDrop": "Перетащите файлы или каталоги",
"LabelUploaderDropFiles": "Перетащите файлы", "LabelUploaderDropFiles": "Перетащите файлы",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUploaderItemFetchMetadataHelp": "Автоматическое извлечение названия, автора и серии",
"LabelUseChapterTrack": "Показывать время главы", "LabelUseChapterTrack": "Показывать время главы",
"LabelUseFullTrack": "Показывать время книги", "LabelUseFullTrack": "Показывать время книги",
"LabelUser": "Пользователь", "LabelUser": "Пользователь",
@ -557,17 +557,17 @@
"MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?", "MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?",
"MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?", "MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?",
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?", "MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", "MessageConfirmDeleteLibraryItem": "Это приведет к удалению элемента библиотеки из базы данных и файловой системы. Вы уверены?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteLibraryItems": "Это приведет к удалению {0} элементов библиотеки из базы данных и файловой системы. Вы уверены?",
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?", "MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?", "MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?", "MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?",
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?", "MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?",
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?", "MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?",
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?", "MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmQuickEmbed": "Предупреждение! Быстрое встраивание не позволяет создавать резервные копии аудиофайлов. Убедитесь, что у вас есть резервная копия аудиофайлов. <br><br>Хотите продолжить?",
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?", "MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Вы уверены, что хотите удалить автора \"{0}\"?",
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?", "MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?", "MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?", "MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
@ -579,7 +579,7 @@
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?", "MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.", "MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".", "MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmReScanLibraryItems": "Вы уверены, что хотите пересканировать {0} элементов?",
"MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?", "MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?",
"MessageDownloadingEpisode": "Эпизод скачивается", "MessageDownloadingEpisode": "Эпизод скачивается",
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков", "MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",

View file

@ -1,10 +1,10 @@
{ {
"ButtonAdd": "增加", "ButtonAdd": "增加",
"ButtonAddChapters": "添加章节", "ButtonAddChapters": "添加章节",
"ButtonAddDevice": "Add Device", "ButtonAddDevice": "添加设备",
"ButtonAddLibrary": "Add Library", "ButtonAddLibrary": "添加库",
"ButtonAddPodcasts": "添加播客", "ButtonAddPodcasts": "添加播客",
"ButtonAddUser": "Add User", "ButtonAddUser": "添加用户",
"ButtonAddYourFirstLibrary": "添加第一个媒体库", "ButtonAddYourFirstLibrary": "添加第一个媒体库",
"ButtonApply": "应用", "ButtonApply": "应用",
"ButtonApplyChapters": "应用到章节", "ButtonApplyChapters": "应用到章节",
@ -62,7 +62,7 @@
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除", "ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
"ButtonReScan": "重新扫描", "ButtonReScan": "重新扫描",
"ButtonReset": "重置", "ButtonReset": "重置",
"ButtonResetToDefault": "Reset to default", "ButtonResetToDefault": "重置为默认",
"ButtonRestore": "恢复", "ButtonRestore": "恢复",
"ButtonSave": "保存", "ButtonSave": "保存",
"ButtonSaveAndClose": "保存并关闭", "ButtonSaveAndClose": "保存并关闭",
@ -87,15 +87,15 @@
"ButtonUserEdit": "编辑用户 {0}", "ButtonUserEdit": "编辑用户 {0}",
"ButtonViewAll": "查看全部", "ButtonViewAll": "查看全部",
"ButtonYes": "确定", "ButtonYes": "确定",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata", "ErrorUploadFetchMetadataAPI": "获取元数据时出错",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author", "ErrorUploadFetchMetadataNoResults": "无法获取元数据 - 尝试更新标题和/或作者",
"ErrorUploadLacksTitle": "Must have a title", "ErrorUploadLacksTitle": "必须有标题",
"HeaderAccount": "帐户", "HeaderAccount": "帐户",
"HeaderAdvanced": "高级", "HeaderAdvanced": "高级",
"HeaderAppriseNotificationSettings": "测试通知设置", "HeaderAppriseNotificationSettings": "测试通知设置",
"HeaderAudiobookTools": "有声读物文件管理工具", "HeaderAudiobookTools": "有声读物文件管理工具",
"HeaderAudioTracks": "音轨", "HeaderAudioTracks": "音轨",
"HeaderAuthentication": "Authentication", "HeaderAuthentication": "身份验证",
"HeaderBackups": "备份", "HeaderBackups": "备份",
"HeaderChangePassword": "更改密码", "HeaderChangePassword": "更改密码",
"HeaderChapters": "章节", "HeaderChapters": "章节",
@ -130,15 +130,15 @@
"HeaderManageTags": "管理标签", "HeaderManageTags": "管理标签",
"HeaderMapDetails": "编辑详情", "HeaderMapDetails": "编辑详情",
"HeaderMatch": "匹配", "HeaderMatch": "匹配",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence", "HeaderMetadataOrderOfPrecedence": "元数据优先级",
"HeaderMetadataToEmbed": "嵌入元数据", "HeaderMetadataToEmbed": "嵌入元数据",
"HeaderNewAccount": "新建帐户", "HeaderNewAccount": "新建帐户",
"HeaderNewLibrary": "新建媒体库", "HeaderNewLibrary": "新建媒体库",
"HeaderNotifications": "通知", "HeaderNotifications": "通知",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenIDConnectAuthentication": "OpenID 连接身份验证",
"HeaderOpenRSSFeed": "打开 RSS 源", "HeaderOpenRSSFeed": "打开 RSS 源",
"HeaderOtherFiles": "其他文件", "HeaderOtherFiles": "其他文件",
"HeaderPasswordAuthentication": "Password Authentication", "HeaderPasswordAuthentication": "密码认证",
"HeaderPermissions": "权限", "HeaderPermissions": "权限",
"HeaderPlayerQueue": "播放队列", "HeaderPlayerQueue": "播放队列",
"HeaderPlaylist": "播放列表", "HeaderPlaylist": "播放列表",
@ -187,11 +187,11 @@
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏", "LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
"LabelAddToPlaylist": "添加到播放列表", "LabelAddToPlaylist": "添加到播放列表",
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表", "LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
"LabelAdminUsersOnly": "Admin users only", "LabelAdminUsersOnly": "仅限管理员用户",
"LabelAll": "全部", "LabelAll": "全部",
"LabelAllUsers": "所有用户", "LabelAllUsers": "所有用户",
"LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersExcludingGuests": "除访客外的所有用户",
"LabelAllUsersIncludingGuests": "All users including guests", "LabelAllUsersIncludingGuests": "包括访客的所有用户",
"LabelAlreadyInYourLibrary": "已存在你的库中", "LabelAlreadyInYourLibrary": "已存在你的库中",
"LabelAppend": "附加", "LabelAppend": "附加",
"LabelAuthor": "作者", "LabelAuthor": "作者",
@ -199,12 +199,12 @@
"LabelAuthorLastFirst": "作者 (名, 姓)", "LabelAuthorLastFirst": "作者 (名, 姓)",
"LabelAuthors": "作者", "LabelAuthors": "作者",
"LabelAutoDownloadEpisodes": "自动下载剧集", "LabelAutoDownloadEpisodes": "自动下载剧集",
"LabelAutoFetchMetadata": "Auto Fetch Metadata", "LabelAutoFetchMetadata": "自动获取元数据",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.", "LabelAutoFetchMetadataHelp": "获取标题, 作者和系列的元数据以简化上传. 上传后可能需要匹配其他元数据.",
"LabelAutoLaunch": "Auto Launch", "LabelAutoLaunch": "自动启动",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)", "LabelAutoLaunchDescription": "导航到登录页面时自动重定向到身份验证提供程序 (手动覆盖路径 <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register", "LabelAutoRegister": "自动注册",
"LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelAutoRegisterDescription": "登录后自动创建新用户",
"LabelBackToUser": "返回到用户", "LabelBackToUser": "返回到用户",
"LabelBackupLocation": "备份位置", "LabelBackupLocation": "备份位置",
"LabelBackupsEnableAutomaticBackups": "启用自动备份", "LabelBackupsEnableAutomaticBackups": "启用自动备份",
@ -215,13 +215,13 @@
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.", "LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
"LabelBitrate": "比特率", "LabelBitrate": "比特率",
"LabelBooks": "图书", "LabelBooks": "图书",
"LabelButtonText": "Button Text", "LabelButtonText": "按钮文本",
"LabelChangePassword": "修改密码", "LabelChangePassword": "修改密码",
"LabelChannels": "声道", "LabelChannels": "声道",
"LabelChapters": "章节", "LabelChapters": "章节",
"LabelChaptersFound": "找到的章节", "LabelChaptersFound": "找到的章节",
"LabelChapterTitle": "章节标题", "LabelChapterTitle": "章节标题",
"LabelClickForMoreInfo": "Click for more info", "LabelClickForMoreInfo": "点击了解更多信息",
"LabelClosePlayer": "关闭播放器", "LabelClosePlayer": "关闭播放器",
"LabelCodec": "编解码", "LabelCodec": "编解码",
"LabelCollapseSeries": "折叠系列", "LabelCollapseSeries": "折叠系列",
@ -240,12 +240,12 @@
"LabelCurrently": "当前:", "LabelCurrently": "当前:",
"LabelCustomCronExpression": "自定义计划任务表达式:", "LabelCustomCronExpression": "自定义计划任务表达式:",
"LabelDatetime": "日期时间", "LabelDatetime": "日期时间",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)", "LabelDeleteFromFileSystemCheckbox": "从文件系统删除 (取消选中仅从数据库中删除)",
"LabelDescription": "描述", "LabelDescription": "描述",
"LabelDeselectAll": "全部取消选择", "LabelDeselectAll": "全部取消选择",
"LabelDevice": "设备", "LabelDevice": "设备",
"LabelDeviceInfo": "设备信息", "LabelDeviceInfo": "设备信息",
"LabelDeviceIsAvailableTo": "Device is available to...", "LabelDeviceIsAvailableTo": "设备可用于...",
"LabelDirectory": "目录", "LabelDirectory": "目录",
"LabelDiscFromFilename": "从文件名获取光盘", "LabelDiscFromFilename": "从文件名获取光盘",
"LabelDiscFromMetadata": "从元数据获取光盘", "LabelDiscFromMetadata": "从元数据获取光盘",
@ -271,7 +271,7 @@
"LabelExample": "示例", "LabelExample": "示例",
"LabelExplicit": "信息准确", "LabelExplicit": "信息准确",
"LabelFeedURL": "源 URL", "LabelFeedURL": "源 URL",
"LabelFetchingMetadata": "Fetching Metadata", "LabelFetchingMetadata": "正在获取元数据",
"LabelFile": "文件", "LabelFile": "文件",
"LabelFileBirthtime": "文件创建时间", "LabelFileBirthtime": "文件创建时间",
"LabelFileModified": "文件修改时间", "LabelFileModified": "文件修改时间",
@ -289,7 +289,7 @@
"LabelHardDeleteFile": "完全删除文件", "LabelHardDeleteFile": "完全删除文件",
"LabelHasEbook": "有电子书", "LabelHasEbook": "有电子书",
"LabelHasSupplementaryEbook": "有补充电子书", "LabelHasSupplementaryEbook": "有补充电子书",
"LabelHighestPriority": "Highest priority", "LabelHighestPriority": "最高优先级",
"LabelHost": "主机", "LabelHost": "主机",
"LabelHour": "小时", "LabelHour": "小时",
"LabelIcon": "图标", "LabelIcon": "图标",
@ -331,20 +331,20 @@
"LabelLogLevelInfo": "信息", "LabelLogLevelInfo": "信息",
"LabelLogLevelWarn": "警告", "LabelLogLevelWarn": "警告",
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
"LabelLowestPriority": "Lowest Priority", "LabelLowestPriority": "最低优先级",
"LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersBy": "匹配现有用户",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过SSO提供商提供的唯一 id 进行匹配",
"LabelMediaPlayer": "媒体播放器", "LabelMediaPlayer": "媒体播放器",
"LabelMediaType": "媒体类型", "LabelMediaType": "媒体类型",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataOrderOfPrecedenceDescription": "较高优先级的元数据源将覆盖较低优先级的元数据源",
"LabelMetadataProvider": "元数据提供者", "LabelMetadataProvider": "元数据提供者",
"LabelMetaTag": "元数据标签", "LabelMetaTag": "元数据标签",
"LabelMetaTags": "元标签", "LabelMetaTags": "元标签",
"LabelMinute": "分钟", "LabelMinute": "分钟",
"LabelMissing": "丢失", "LabelMissing": "丢失",
"LabelMissingParts": "丢失的部分", "LabelMissingParts": "丢失的部分",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs", "LabelMobileRedirectURIs": "允许移动应用重定向 URI",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.", "LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.",
"LabelMore": "更多", "LabelMore": "更多",
"LabelMoreInfo": "更多..", "LabelMoreInfo": "更多..",
"LabelName": "名称", "LabelName": "名称",
@ -523,7 +523,7 @@
"LabelUpdateDetailsHelp": "找到匹配项时允许覆盖所选书籍存在的详细信息", "LabelUpdateDetailsHelp": "找到匹配项时允许覆盖所选书籍存在的详细信息",
"LabelUploaderDragAndDrop": "拖放文件或文件夹", "LabelUploaderDragAndDrop": "拖放文件或文件夹",
"LabelUploaderDropFiles": "删除文件", "LabelUploaderDropFiles": "删除文件",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUploaderItemFetchMetadataHelp": "自动获取标题, 作者和系列",
"LabelUseChapterTrack": "使用章节音轨", "LabelUseChapterTrack": "使用章节音轨",
"LabelUseFullTrack": "使用完整音轨", "LabelUseFullTrack": "使用完整音轨",
"LabelUser": "用户", "LabelUser": "用户",
@ -557,15 +557,15 @@
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?", "MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?", "MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?", "MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?", "MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?", "MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?",
"MessageConfirmDeleteSession": "你确定要删除此会话吗?", "MessageConfirmDeleteSession": "你确定要删除此会话吗?",
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?", "MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?", "MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?", "MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?", "MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?", "MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. <br><br>你是否想继续吗?",
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?", "MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
"MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?", "MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?",
"MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?", "MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?",
@ -579,7 +579,7 @@
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?", "MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.", "MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".", "MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmReScanLibraryItems": "你确定要重新扫描 {0} 个项目吗?",
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?", "MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
"MessageDownloadingEpisode": "正在下载剧集", "MessageDownloadingEpisode": "正在下载剧集",
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序", "MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",

View file

@ -3,12 +3,28 @@ version: "3.7"
services: services:
audiobookshelf: audiobookshelf:
image: ghcr.io/advplyr/audiobookshelf image: ghcr.io/advplyr/audiobookshelf:latest
# ABS runs on port 13378 by default. If you want to change
# the port, only change the external port, not the internal port
ports: ports:
- 13378:80 - 13378:80
volumes: volumes:
# These volumes are needed to keep your library persistent
# and allow media to be accessed by the ABS server.
# The path to the left of the colon is the path on your computer,
# and the path to the right of the colon is where the data is
# available to ABS in Docker.
# You can change these media directories or add as many as you want
- ./audiobooks:/audiobooks - ./audiobooks:/audiobooks
- ./podcasts:/podcasts - ./podcasts:/podcasts
# The metadata directory can be stored anywhere on your computer
- ./metadata:/metadata - ./metadata:/metadata
# The config directory needs to be on the same physical machine
# you are running ABS on
- ./config:/config - ./config:/config
restart: unless-stopped restart: unless-stopped
# You can use the following environment variable to run the ABS
# docker container as a specific user. You will need to change
# the UID and GID to the correct values for your user.
#environment:
# - user=1000:1000

View file

@ -3,15 +3,18 @@ const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder') const PodcastFinder = require('../finders/PodcastFinder')
const AuthorFinder = require('../finders/AuthorFinder') const AuthorFinder = require('../finders/AuthorFinder')
const MusicFinder = require('../finders/MusicFinder') const MusicFinder = require('../finders/MusicFinder')
const Database = require("../Database")
class SearchController { class SearchController {
constructor() { } constructor() { }
async findBooks(req, res) { async findBooks(req, res) {
const id = req.query.id
const libraryItem = await Database.libraryItemModel.getOldById(id)
const provider = req.query.provider || 'google' const provider = req.query.provider || 'google'
const title = req.query.title || '' const title = req.query.title || ''
const author = req.query.author || '' const author = req.query.author || ''
const results = await BookFinder.search(provider, title, author) const results = await BookFinder.search(libraryItem, provider, title, author)
res.json(results) res.json(results)
} }

View file

@ -167,6 +167,7 @@ class BookFinder {
[/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition [/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type [/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
[/ a novel.*$/g, ''], // Remove "a novel" [/ a novel.*$/g, ''], // Remove "a novel"
[/(^| )(un)?abridged( |$)/g, ' '], // Remove "unabridged/abridged"
[/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers [/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers
] ]
@ -298,6 +299,7 @@ class BookFinder {
/** /**
* Search for books including fuzzy searches * Search for books including fuzzy searches
* *
* @param {Object} libraryItem
* @param {string} provider * @param {string} provider
* @param {string} title * @param {string} title
* @param {string} author * @param {string} author
@ -306,7 +308,7 @@ class BookFinder {
* @param {{titleDistance:number, authorDistance:number, maxFuzzySearches:number}} options * @param {{titleDistance:number, authorDistance:number, maxFuzzySearches:number}} options
* @returns {Promise<Object[]>} * @returns {Promise<Object[]>}
*/ */
async search(provider, title, author, isbn, asin, options = {}) { async search(libraryItem, provider, title, author, isbn, asin, options = {}) {
let books = [] let books = []
const maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4 const maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
const maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4 const maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
@ -335,6 +337,7 @@ class BookFinder {
for (const titlePart of titleParts) for (const titlePart of titleParts)
authorCandidates.add(titlePart) authorCandidates.add(titlePart)
authorCandidates = await authorCandidates.getCandidates() authorCandidates = await authorCandidates.getCandidates()
loop_author:
for (const authorCandidate of authorCandidates) { for (const authorCandidate of authorCandidates) {
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate) let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
for (const titlePart of titleParts) for (const titlePart of titleParts)
@ -342,13 +345,27 @@ class BookFinder {
titleCandidates = titleCandidates.getCandidates() titleCandidates = titleCandidates.getCandidates()
for (const titleCandidate of titleCandidates) { for (const titleCandidate of titleCandidates) {
if (titleCandidate == title && authorCandidate == author) continue // We already tried this if (titleCandidate == title && authorCandidate == author) continue // We already tried this
if (++numFuzzySearches > maxFuzzySearches) return books if (++numFuzzySearches > maxFuzzySearches) break loop_author
books = await this.runSearch(titleCandidate, authorCandidate, provider, asin, maxTitleDistance, maxAuthorDistance) books = await this.runSearch(titleCandidate, authorCandidate, provider, asin, maxTitleDistance, maxAuthorDistance)
if (books.length) return books if (books.length) break loop_author
} }
} }
} }
if (books.length) {
const resultsHaveDuration = provider.startsWith('audible')
if (resultsHaveDuration && libraryItem?.media?.duration) {
const libraryItemDurationMinutes = libraryItem.media.duration / 60
// If provider results have duration, sort by ascendinge duration difference from libraryItem
books.sort((a, b) => {
const aDuration = a.duration || Number.POSITIVE_INFINITY
const bDuration = b.duration || Number.POSITIVE_INFINITY
const aDurationDiff = Math.abs(aDuration - libraryItemDurationMinutes)
const bDurationDiff = Math.abs(bDuration - libraryItemDurationMinutes)
return aDurationDiff - bDurationDiff
})
}
}
return books return books
} }
@ -392,12 +409,12 @@ class BookFinder {
if (provider === 'all') { if (provider === 'all') {
for (const providerString of this.providers) { for (const providerString of this.providers) {
const providerResults = await this.search(providerString, title, author, options) const providerResults = await this.search(null, providerString, title, author, options)
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`) Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
searchResults.push(...providerResults) searchResults.push(...providerResults)
} }
} else { } else {
searchResults = await this.search(provider, title, author, options) searchResults = await this.search(null, provider, title, author, options)
} }
Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`) Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`)
@ -461,6 +478,8 @@ function cleanAuthorForCompares(author) {
cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2') cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2')
// remove middle initials // remove middle initials
cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '') cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '')
// remove et al.
cleanAuthor = cleanAuthor.replace(/ et al\.?(?= |$)/g, '')
return cleanAuthor return cleanAuthor
} }

View file

@ -103,19 +103,29 @@ class RssFeedManager {
await Database.updateFeed(feed) await Database.updateFeed(feed)
} }
} else if (feed.entityType === 'collection') { } else if (feed.entityType === 'collection') {
const collection = await Database.collectionModel.findByPk(feed.entityId) const collection = await Database.collectionModel.findByPk(feed.entityId, {
include: Database.collectionBookModel
})
if (collection) { if (collection) {
const collectionExpanded = await collection.getOldJsonExpanded() const collectionExpanded = await collection.getOldJsonExpanded()
// Find most recently updated item in collection // Find most recently updated item in collection
let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate let mostRecentlyUpdatedAt = collectionExpanded.lastUpdate
// Check for most recently updated book
collectionExpanded.books.forEach((libraryItem) => { collectionExpanded.books.forEach((libraryItem) => {
if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) { if (libraryItem.media.tracks.length && libraryItem.updatedAt > mostRecentlyUpdatedAt) {
mostRecentlyUpdatedAt = libraryItem.updatedAt mostRecentlyUpdatedAt = libraryItem.updatedAt
} }
}) })
// Check for most recently added collection book
collection.collectionBooks.forEach((collectionBook) => {
if (collectionBook.createdAt.valueOf() > mostRecentlyUpdatedAt) {
mostRecentlyUpdatedAt = collectionBook.createdAt.valueOf()
}
})
const hasBooksRemoved = collection.collectionBooks.length < feed.episodes.length
if (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt) { if (!feed.entityUpdatedAt || hasBooksRemoved || mostRecentlyUpdatedAt > feed.entityUpdatedAt) {
Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`) Logger.debug(`[RssFeedManager] Updating RSS feed for collection "${collection.name}"`)
feed.updateFromCollection(collectionExpanded) feed.updateFromCollection(collectionExpanded)

View file

@ -108,7 +108,7 @@ class Feed extends Model {
/** /**
* Find all library item ids that have an open feed (used in library filter) * Find all library item ids that have an open feed (used in library filter)
* @returns {Promise<Array<String>>} array of library item ids * @returns {Promise<string[]>} array of library item ids
*/ */
static async findAllLibraryItemIds() { static async findAllLibraryItemIds() {
const feeds = await this.findAll({ const feeds = await this.findAll({
@ -122,8 +122,8 @@ class Feed extends Model {
/** /**
* Find feed where and return oldFeed * Find feed where and return oldFeed
* @param {object} where sequelize where object * @param {Object} where sequelize where object
* @returns {Promise<objects.Feed>} oldFeed * @returns {Promise<oldFeed>} oldFeed
*/ */
static async findOneOld(where) { static async findOneOld(where) {
if (!where) return null if (!where) return null
@ -140,7 +140,7 @@ class Feed extends Model {
/** /**
* Find feed and return oldFeed * Find feed and return oldFeed
* @param {string} id * @param {string} id
* @returns {Promise<objects.Feed>} oldFeed * @returns {Promise<oldFeed>} oldFeed
*/ */
static async findByPkOld(id) { static async findByPkOld(id) {
if (!id) return null if (!id) return null

View file

@ -37,7 +37,7 @@ class Scanner {
var searchISBN = options.isbn || libraryItem.media.metadata.isbn var searchISBN = options.isbn || libraryItem.media.metadata.isbn
var searchASIN = options.asin || libraryItem.media.metadata.asin var searchASIN = options.asin || libraryItem.media.metadata.asin
var results = await BookFinder.search(provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 }) var results = await BookFinder.search(libraryItem, provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 })
if (!results.length) { if (!results.length) {
return { return {
warning: `No ${provider} match found` warning: `No ${provider} match found`

View file

@ -218,7 +218,7 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal
module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`) Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`)
return axios.get(feedUrl, { timeout: 12000, responseType: 'arraybuffer' }).then(async (data) => { return axios.get(feedUrl, { timeout: 12000, responseType: 'arraybuffer', headers: { Accept: 'application/rss+xml' } }).then(async (data) => {
// Adding support for ios-8859-1 encoded RSS feeds. // Adding support for ios-8859-1 encoded RSS feeds.
// See: https://github.com/advplyr/audiobookshelf/issues/1489 // See: https://github.com/advplyr/audiobookshelf/issues/1489

View file

@ -35,6 +35,8 @@ describe('TitleCandidates', () => {
['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']], ['adds candidate + variant, removing edition 2', 'anna karenina 4th ed.', ['anna karenina', 'anna karenina 4th ed.']],
['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']], ['adds candidate + variant, removing fie type', 'anna karenina.mp3', ['anna karenina', 'anna karenina.mp3']],
['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']], ['adds candidate + variant, removing "a novel"', 'anna karenina a novel', ['anna karenina', 'anna karenina a novel']],
['adds candidate + variant, removing "abridged"', 'abridged anna karenina', ['anna karenina', 'abridged anna karenina']],
['adds candidate + variant, removing "unabridged"', 'anna karenina unabridged', ['anna karenina', 'anna karenina unabridged']],
['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']], ['adds candidate + variant, removing preceding/trailing numbers', '1 anna karenina 2', ['anna karenina', '1 anna karenina 2']],
['does not add empty candidate', '', []], ['does not add empty candidate', '', []],
['does not add spaces-only candidate', ' ', []], ['does not add spaces-only candidate', ' ', []],
@ -109,6 +111,7 @@ describe('AuthorCandidates', () => {
['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']], ['adds recognized author if edit distance from candidate is small', 'nicolai gogol', ['nikolai gogol']],
['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []], ['does not add candidate if edit distance from any recognized author is large', 'nikolai google', []],
['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']], ['adds normalized recognized candidate (contains redundant spaces)', 'nikolai gogol', ['nikolai gogol']],
['adds normalized recognized candidate (et al removed)', 'nikolai gogol et al.', ['nikolai gogol']],
['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']], ['adds normalized recognized candidate (normalized initials)', 'j.k. rowling', ['j. k. rowling']],
].forEach(([name, author, expected]) => it(name, async () => { ].forEach(([name, author, expected]) => it(name, async () => {
authorCandidates.add(author) authorCandidates.add(author)
@ -222,14 +225,14 @@ describe('search', () => {
describe('search title is empty', () => { describe('search title is empty', () => {
it('returns empty result', async () => { it('returns empty result', async () => {
expect(await bookFinder.search('', '', a)).to.deep.equal([]) expect(await bookFinder.search(null, '', '', a)).to.deep.equal([])
sinon.assert.callCount(bookFinder.runSearch, 0) sinon.assert.callCount(bookFinder.runSearch, 0)
}) })
}) })
describe('search title is a recognized title and search author is a recognized author', () => { describe('search title is a recognized title and search author is a recognized author', () => {
it('returns non-empty result (no fuzzy searches)', async () => { it('returns non-empty result (no fuzzy searches)', async () => {
expect(await bookFinder.search('', t, a)).to.deep.equal(r) expect(await bookFinder.search(null, '', t, a)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 1) sinon.assert.callCount(bookFinder.runSearch, 1)
}) })
}) })
@ -251,7 +254,7 @@ describe('search', () => {
[`2022_${t}_HQ`], [`2022_${t}_HQ`],
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => { it(`search('${searchTitle}', '${a}') returns non-empty result (with 1 fuzzy search)`, async () => {
expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r) expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 2) sinon.assert.callCount(bookFinder.runSearch, 2)
}) })
}); });
@ -261,7 +264,7 @@ describe('search', () => {
[`${a} - series 01 - ${t}`], [`${a} - series 01 - ${t}`],
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => { it(`search('${searchTitle}', '${a}') returns non-empty result (with 2 fuzzy searches)`, async () => {
expect(await bookFinder.search('', searchTitle, a)).to.deep.equal(r) expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 3) sinon.assert.callCount(bookFinder.runSearch, 3)
}) })
}); });
@ -271,7 +274,7 @@ describe('search', () => {
[`${t} junk`], [`${t} junk`],
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns an empty result`, async () => { it(`search('${searchTitle}', '${a}') returns an empty result`, async () => {
expect(await bookFinder.search('', searchTitle, a)).to.deep.equal([]) expect(await bookFinder.search(null, '', searchTitle, a)).to.deep.equal([])
}) })
}) })
@ -280,7 +283,7 @@ describe('search', () => {
[`${t} - ${a}`], [`${t} - ${a}`],
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => { it(`search('${searchTitle}', '${a}') returns an empty result (with no fuzzy searches)`, async () => {
expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([]) expect(await bookFinder.search(null, '', searchTitle, a, null, null, { maxFuzzySearches: 0 })).to.deep.equal([])
sinon.assert.callCount(bookFinder.runSearch, 1) sinon.assert.callCount(bookFinder.runSearch, 1)
}) })
}) })
@ -292,7 +295,7 @@ describe('search', () => {
[`${a} - series 01 - ${t}`], [`${a} - series 01 - ${t}`],
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => { it(`search('${searchTitle}', '${a}') returns an empty result (1 fuzzy search)`, async () => {
expect(await bookFinder.search('', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([]) expect(await bookFinder.search(null, '', searchTitle, a, null, null, { maxFuzzySearches: 1 })).to.deep.equal([])
sinon.assert.callCount(bookFinder.runSearch, 2) sinon.assert.callCount(bookFinder.runSearch, 2)
}) })
}) })
@ -305,7 +308,7 @@ describe('search', () => {
[`${a} - ${t}`], [`${a} - ${t}`],
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => { it(`search('${searchTitle}', '') returns a non-empty result (1 fuzzy search)`, async () => {
expect(await bookFinder.search('', searchTitle, '')).to.deep.equal(r) expect(await bookFinder.search(null, '', searchTitle, '')).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 2) sinon.assert.callCount(bookFinder.runSearch, 2)
}) })
}); });
@ -316,7 +319,7 @@ describe('search', () => {
[`${u} - ${t}`] [`${u} - ${t}`]
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '') returns an empty result`, async () => { it(`search('${searchTitle}', '') returns an empty result`, async () => {
expect(await bookFinder.search('', searchTitle, '')).to.deep.equal([]) expect(await bookFinder.search(null, '', searchTitle, '')).to.deep.equal([])
}) })
}) })
}) })
@ -327,7 +330,7 @@ describe('search', () => {
[`${u} - ${t}`] [`${u} - ${t}`]
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => { it(`search('${searchTitle}', '${u}') returns a non-empty result (1 fuzzy search)`, async () => {
expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r) expect(await bookFinder.search(null, '', searchTitle, u)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 2) sinon.assert.callCount(bookFinder.runSearch, 2)
}) })
}); });
@ -336,9 +339,41 @@ describe('search', () => {
[`${t}`] [`${t}`]
].forEach(([searchTitle]) => { ].forEach(([searchTitle]) => {
it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => { it(`search('${searchTitle}', '${u}') returns a non-empty result (no fuzzy search)`, async () => {
expect(await bookFinder.search('', searchTitle, u)).to.deep.equal(r) expect(await bookFinder.search(null, '', searchTitle, u)).to.deep.equal(r)
sinon.assert.callCount(bookFinder.runSearch, 1) sinon.assert.callCount(bookFinder.runSearch, 1)
}) })
}) })
}) })
describe('search provider results have duration', () => {
const libraryItem = { media: { duration: 60 * 1000 } }
const provider = 'audible'
const unsorted = [{ duration: 3000 }, { duration: 2000 }, { duration: 1000 }, { duration: 500 }]
const sorted = [{ duration: 1000 }, { duration: 500 }, { duration: 2000 }, { duration: 3000 }]
runSearchStub.withArgs(t, a, provider).resolves(unsorted)
it('returns results sorted by library item duration diff', async () => {
expect(await bookFinder.search(libraryItem, provider, t, a)).to.deep.equal(sorted)
})
it('returns unsorted results if library item is null', async () => {
expect(await bookFinder.search(null, provider, t, a)).to.deep.equal(unsorted)
})
it('returns unsorted results if library item duration is undefined', async () => {
expect(await bookFinder.search({ media: {} }, provider, t, a)).to.deep.equal(unsorted)
})
it('returns unsorted results if library item media is undefined', async () => {
expect(await bookFinder.search({ }, provider, t, a)).to.deep.equal(unsorted)
})
it ('should return a result last if it has no duration', async () => {
const unsorted = [{}, { duration: 3000 }, { duration: 2000 }, { duration: 1000 }, { duration: 500 }]
const sorted = [{ duration: 1000 }, { duration: 500 }, { duration: 2000 }, { duration: 3000 }, {}]
runSearchStub.withArgs(t, a, provider).resolves(unsorted)
expect(await bookFinder.search(libraryItem, provider, t, a)).to.deep.equal(sorted)
})
})
}) })