mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-05-19 18:01:37 +00:00
Merge branch 'advplyr:master' into auto-generate-chapters-from-timestamps
This commit is contained in:
commit
64fd42ebf4
26 changed files with 197 additions and 67 deletions
4
client/package-lock.json
generated
4
client/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.33.1",
|
"version": "2.33.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.33.1",
|
"version": "2.33.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.33.1",
|
"version": "2.33.2",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,20 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||||
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
||||||
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
|
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
|
||||||
|
|
||||||
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm']
|
var mimeTypes = [
|
||||||
|
'audio/flac',
|
||||||
|
'audio/mpeg',
|
||||||
|
'audio/mp4',
|
||||||
|
'audio/ogg',
|
||||||
|
'audio/aac',
|
||||||
|
'audio/x-ms-wma',
|
||||||
|
'audio/x-aiff',
|
||||||
|
'audio/webm',
|
||||||
|
// `audio/matroska` is the correct mimetype, but the server still uses `audio/x-matroska`
|
||||||
|
// ref: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||||
|
'audio/matroska',
|
||||||
|
'audio/x-matroska'
|
||||||
|
]
|
||||||
var mimeTypeCanPlayMap = {}
|
var mimeTypeCanPlayMap = {}
|
||||||
mimeTypes.forEach((mt) => {
|
mimeTypes.forEach((mt) => {
|
||||||
var canPlay = this.player.canPlayType(mt)
|
var canPlay = this.player.canPlayType(mt)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"ButtonBrowseForFolder": "Агляд папак",
|
"ButtonBrowseForFolder": "Агляд папак",
|
||||||
"ButtonCancel": "Скасаваць",
|
"ButtonCancel": "Скасаваць",
|
||||||
"ButtonCancelEncode": "Скасаваць кадзіраванне",
|
"ButtonCancelEncode": "Скасаваць кадзіраванне",
|
||||||
"ButtonChangeRootPassword": "Зменіце Root пароль",
|
"ButtonChangeRootPassword": "Змяніць пароль root",
|
||||||
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі",
|
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі",
|
||||||
"ButtonChooseAFolder": "Выбраць папку",
|
"ButtonChooseAFolder": "Выбраць папку",
|
||||||
"ButtonChooseFiles": "Выбраць файлы",
|
"ButtonChooseFiles": "Выбраць файлы",
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
"ButtonRemove": "Выдаліць",
|
"ButtonRemove": "Выдаліць",
|
||||||
"ButtonRemoveAll": "Выдаліць усе",
|
"ButtonRemoveAll": "Выдаліць усе",
|
||||||
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
||||||
"ButtonRemoveFromContinueListening": "Выдаліць з Працягнуць праслухоўванне",
|
"ButtonRemoveFromContinueListening": "Выдаліць з Працяг праслухоўвання",
|
||||||
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
|
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
|
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
|
||||||
"ButtonReset": "Скінуць",
|
"ButtonReset": "Скінуць",
|
||||||
|
|
@ -252,8 +252,8 @@
|
||||||
"LabelAudioChannels": "Аўдыяканалы (1 або 2)",
|
"LabelAudioChannels": "Аўдыяканалы (1 або 2)",
|
||||||
"LabelAudioCodec": "Аўдыякодэк",
|
"LabelAudioCodec": "Аўдыякодэк",
|
||||||
"LabelAuthor": "Аўтар",
|
"LabelAuthor": "Аўтар",
|
||||||
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)",
|
"LabelAuthorFirstLast": "Аўтар (імя, прозвішча)",
|
||||||
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
|
"LabelAuthorLastFirst": "Аўтар (прозвішча, імя)",
|
||||||
"LabelAuthors": "Аўтары",
|
"LabelAuthors": "Аўтары",
|
||||||
"LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі",
|
"LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі",
|
||||||
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых",
|
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых",
|
||||||
|
|
@ -292,7 +292,7 @@
|
||||||
"LabelCollections": "Калекцыі",
|
"LabelCollections": "Калекцыі",
|
||||||
"LabelComplete": "Завяршыць",
|
"LabelComplete": "Завяршыць",
|
||||||
"LabelConfirmPassword": "Пацвердзіце пароль",
|
"LabelConfirmPassword": "Пацвердзіце пароль",
|
||||||
"LabelContinueListening": "Працягнуць праслухоўванне",
|
"LabelContinueListening": "Працяг праслухоўвання",
|
||||||
"LabelContinueReading": "Працягнуць чытанне",
|
"LabelContinueReading": "Працягнуць чытанне",
|
||||||
"LabelContinueSeries": "Працягнуць серыі",
|
"LabelContinueSeries": "Працягнуць серыі",
|
||||||
"LabelCorsAllowed": "Дазволеныя крыніцы CORS",
|
"LabelCorsAllowed": "Дазволеныя крыніцы CORS",
|
||||||
|
|
@ -424,7 +424,7 @@
|
||||||
"LabelLastBookAdded": "Апошняя дададзеная кніга",
|
"LabelLastBookAdded": "Апошняя дададзеная кніга",
|
||||||
"LabelLastBookUpdated": "Апошняя абноўленая кніга",
|
"LabelLastBookUpdated": "Апошняя абноўленая кніга",
|
||||||
"LabelLastProgressDate": "Апошні прагрэс: {0}",
|
"LabelLastProgressDate": "Апошні прагрэс: {0}",
|
||||||
"LabelLastSeen": "Апошні прагляд",
|
"LabelLastSeen": "Апошняя актыўнасць",
|
||||||
"LabelLastTime": "Апошні раз",
|
"LabelLastTime": "Апошні раз",
|
||||||
"LabelLastUpdate": "Апошняе абнаўленне",
|
"LabelLastUpdate": "Апошняе абнаўленне",
|
||||||
"LabelLayout": "Знешні выгляд",
|
"LabelLayout": "Знешні выгляд",
|
||||||
|
|
@ -545,7 +545,7 @@
|
||||||
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
|
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
|
||||||
"LabelRSSFeedURL": "URL RSS-стужкі",
|
"LabelRSSFeedURL": "URL RSS-стужкі",
|
||||||
"LabelRandomly": "Выпадкова",
|
"LabelRandomly": "Выпадкова",
|
||||||
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягнуць праслухоўванне",
|
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працяг праслухоўвання",
|
||||||
"LabelRead": "Чытаць",
|
"LabelRead": "Чытаць",
|
||||||
"LabelReadAgain": "Чытаць зноў",
|
"LabelReadAgain": "Чытаць зноў",
|
||||||
"LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу",
|
"LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу",
|
||||||
|
|
@ -634,12 +634,12 @@
|
||||||
"LabelSortAscending": "Па ўзрастанні",
|
"LabelSortAscending": "Па ўзрастанні",
|
||||||
"LabelSortDescending": "Па ўбыванні",
|
"LabelSortDescending": "Па ўбыванні",
|
||||||
"LabelSortPubDate": "Сартаваць па даце публікацыі",
|
"LabelSortPubDate": "Сартаваць па даце публікацыі",
|
||||||
"LabelStart": "Пачаць",
|
"LabelStart": "Пачатак",
|
||||||
"LabelStartTime": "Час пачатку",
|
"LabelStartTime": "Час пачатку",
|
||||||
"LabelStarted": "Пачата",
|
"LabelStarted": "Пачата",
|
||||||
"LabelStartedAt": "Пачата ў",
|
"LabelStartedAt": "Пачата ў",
|
||||||
"LabelStartedDate": "Пачата {0}",
|
"LabelStartedDate": "Пачата {0}",
|
||||||
"LabelStatsAudioTracks": "Аўдыятрэкаў",
|
"LabelStatsAudioTracks": "Аўдыятрэкі",
|
||||||
"LabelStatsAuthors": "Аўтараў",
|
"LabelStatsAuthors": "Аўтараў",
|
||||||
"LabelStatsBestDay": "Найлепшы дзень",
|
"LabelStatsBestDay": "Найлепшы дзень",
|
||||||
"LabelStatsDailyAverage": "У сярэднім за дзень",
|
"LabelStatsDailyAverage": "У сярэднім за дзень",
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,7 @@
|
||||||
"LabelLibraryFilterSublistEmpty": "Не {0}",
|
"LabelLibraryFilterSublistEmpty": "Не {0}",
|
||||||
"LabelLibraryItem": "Елемент на Библиотека",
|
"LabelLibraryItem": "Елемент на Библиотека",
|
||||||
"LabelLibraryName": "Име на Библиотека",
|
"LabelLibraryName": "Име на Библиотека",
|
||||||
"LabelLibrarySortByProgress": "Прогрес: Последно Обновен",
|
"LabelLibrarySortByProgress": "Прогрес: Последно обновление",
|
||||||
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
|
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
|
||||||
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
|
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
|
||||||
"LabelLimit": "Лимит",
|
"LabelLimit": "Лимит",
|
||||||
|
|
@ -892,7 +892,7 @@
|
||||||
"MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}",
|
"MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}",
|
||||||
"MessageSearchResultsFor": "Резултати от търсенето за",
|
"MessageSearchResultsFor": "Резултати от търсенето за",
|
||||||
"MessageSelected": "{0} избрани",
|
"MessageSelected": "{0} избрани",
|
||||||
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации.",
|
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации",
|
||||||
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
|
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
|
||||||
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
|
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
|
||||||
"MessageShareExpirationWillBe": "Изтичането ще бъде на <strong>{0}</strong>",
|
"MessageShareExpirationWillBe": "Изтичането ще бъде на <strong>{0}</strong>",
|
||||||
|
|
@ -956,6 +956,8 @@
|
||||||
"NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод",
|
"NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод",
|
||||||
"NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити",
|
"NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити",
|
||||||
"NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод",
|
"NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод",
|
||||||
|
"NotificationOnTestDescription": "Event за тестване на системата за нотификации",
|
||||||
|
"PlaceholderBulkChapterInput": "Въведете име на глава или използвайте номериране (прим. 'Епизод 1', 'Глава 10', '1.')",
|
||||||
"PlaceholderNewCollection": "Ново име на колекцията",
|
"PlaceholderNewCollection": "Ново име на колекцията",
|
||||||
"PlaceholderNewFolderPath": "Нов път на папката",
|
"PlaceholderNewFolderPath": "Нов път на папката",
|
||||||
"PlaceholderNewPlaylist": "Ново име на плейлиста",
|
"PlaceholderNewPlaylist": "Ново име на плейлиста",
|
||||||
|
|
@ -963,26 +965,58 @@
|
||||||
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
|
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
|
||||||
"StatsAuthorsAdded": "добаврени автори",
|
"StatsAuthorsAdded": "добаврени автори",
|
||||||
"StatsBooksAdded": "добавени книги",
|
"StatsBooksAdded": "добавени книги",
|
||||||
|
"StatsBooksAdditional": "Някой от вкючените добавки…",
|
||||||
"StatsBooksFinished": "завършени книги",
|
"StatsBooksFinished": "завършени книги",
|
||||||
|
"StatsBooksFinishedThisYear": "Някой от книгите приключени тази година…",
|
||||||
|
"StatsBooksListenedTo": "слушани книги",
|
||||||
|
"StatsCollectionGrewTo": "Твоята книжна колекция израсна до…",
|
||||||
|
"StatsSessions": "сесии",
|
||||||
|
"StatsSpentListening": "прекарано в слушане",
|
||||||
|
"StatsTopAuthor": "ТОП АВТОР",
|
||||||
|
"StatsTopAuthors": "ТОП АВТОРИ",
|
||||||
|
"StatsTopGenre": "ТОП ЖАНР",
|
||||||
|
"StatsTopGenres": "ТОП ЖАНРА",
|
||||||
|
"StatsTopMonth": "ТОП МЕСЕЦ",
|
||||||
|
"StatsTopNarrator": "ТОП РАЗКАЗВАЧ",
|
||||||
|
"StatsTopNarrators": "ТОП РАЗКАЗВАЧИ",
|
||||||
|
"StatsTotalDuration": "С пълно времетраене…",
|
||||||
|
"StatsYearInReview": "ГОДИНАТА В ПРЕГЛЕД",
|
||||||
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
|
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
|
||||||
|
"ToastAppriseUrlRequired": "Трябва да въведете Apprise URL",
|
||||||
|
"ToastAsinRequired": "ASIN-а е задължителен",
|
||||||
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
|
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
|
||||||
|
"ToastAuthorNotFound": "Автор \"{0}\" не е намерен",
|
||||||
|
"ToastAuthorRemoveSuccess": "Арторът е премахнат",
|
||||||
|
"ToastAuthorSearchNotFound": "Авторът не е намерен",
|
||||||
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
|
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
|
||||||
"ToastAuthorUpdateSuccess": "Автора обновен",
|
"ToastAuthorUpdateSuccess": "Автора обновен",
|
||||||
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
|
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
|
||||||
|
"ToastBackupAppliedSuccess": "Архивът е приложен",
|
||||||
"ToastBackupCreateFailed": "Неуспешно създаване на архив",
|
"ToastBackupCreateFailed": "Неуспешно създаване на архив",
|
||||||
"ToastBackupCreateSuccess": "Архивът е създаден",
|
"ToastBackupCreateSuccess": "Архивът е създаден",
|
||||||
"ToastBackupDeleteFailed": "Неуспешно изтриване на архив",
|
"ToastBackupDeleteFailed": "Неуспешно изтриване на архив",
|
||||||
"ToastBackupDeleteSuccess": "Архивът е изтрит",
|
"ToastBackupDeleteSuccess": "Архивът е изтрит",
|
||||||
|
"ToastBackupInvalidMaxKeep": "Невалиден брой за архиви за запазване",
|
||||||
|
"ToastBackupInvalidMaxSize": "Невалиден максимален рамер на архив",
|
||||||
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
|
||||||
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
"ToastBackupUploadFailed": "Неуспешно качване на архив",
|
||||||
"ToastBackupUploadSuccess": "Архивът е качен",
|
"ToastBackupUploadSuccess": "Архивът е качен",
|
||||||
|
"ToastBatchApplyDetailsToItemsSuccess": "Детайли приложени на предмети",
|
||||||
|
"ToastBatchDeleteFailed": "Груповото изтриване се провали",
|
||||||
|
"ToastBatchDeleteSuccess": "Успешно групово изтриване",
|
||||||
|
"ToastBatchQuickMatchFailed": "Груповото Бързо Съвпадение се провали!",
|
||||||
|
"ToastBatchQuickMatchStarted": "Груповото Бързо Съвпадение на {0} книги започна!",
|
||||||
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
|
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
|
||||||
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
|
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
|
||||||
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
||||||
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
||||||
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
||||||
|
"ToastBulkChapterInvalidCount": "Въведете число между 1 и 150",
|
||||||
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
|
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
|
||||||
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
|
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
|
||||||
|
"ToastChapterLocked": "Главата е заключена.",
|
||||||
|
"ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди",
|
||||||
|
"ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.",
|
||||||
"ToastChaptersHaveErrors": "Главите имат грешки",
|
"ToastChaptersHaveErrors": "Главите имат грешки",
|
||||||
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
||||||
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@
|
||||||
"ButtonViewAll": "Alles anzeigen",
|
"ButtonViewAll": "Alles anzeigen",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
|
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
|
||||||
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche, den Titel und/oder den Autor zu aktualisieren.",
|
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden - versuche den Titel und/oder den Autor zu aktualisieren",
|
||||||
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
|
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
"HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
|
"HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
|
||||||
|
|
@ -622,7 +622,7 @@
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
|
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
|
||||||
"LabelSettingsTimeFormat": "Zeitformat",
|
"LabelSettingsTimeFormat": "Zeitformat",
|
||||||
"LabelShare": "Freigeben",
|
"LabelShare": "Freigeben",
|
||||||
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.",
|
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link die Dateien des Mediums als ZIP herunterzuladen.",
|
||||||
"LabelShareOpen": "Freigeben",
|
"LabelShareOpen": "Freigeben",
|
||||||
"LabelShareURL": "Freigabe URL",
|
"LabelShareURL": "Freigabe URL",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
|
|
@ -737,7 +737,7 @@
|
||||||
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||||
"MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.",
|
"MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.",
|
||||||
"MessageAuthenticationLegacyTokenWarning": "Alte API-Token werden in Zukunft entfernt. Benutze stattdessen <a href=\"/config/api-keys\">API Keys</a>.",
|
"MessageAuthenticationLegacyTokenWarning": "Nicht mehr unterstützte API tokens werden in der Zukunft entfernt. Nutze stattdessen <a href=\"/config/api-keys\">API Schlüssel</a>.",
|
||||||
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
|
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
|
||||||
"MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.",
|
"MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.",
|
||||||
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
||||||
|
|
@ -816,7 +816,7 @@
|
||||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||||
"MessageFetching": "Wird abgerufen …",
|
"MessageFetching": "Wird abgerufen …",
|
||||||
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||||
"MessageHeatmapListeningTimeTooltip": "<strong>{0} </strong> auf {1} gehört",
|
"MessageHeatmapListeningTimeTooltip": "<strong>{0} gehört</strong> auf {1}",
|
||||||
"MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}",
|
"MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}",
|
||||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||||
|
|
@ -1103,7 +1103,7 @@
|
||||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||||
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
||||||
"ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert",
|
"ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert",
|
||||||
"ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast-Feeds",
|
"ToastPodcastGetFeedFailed": "Fehler beim Abrufen des Podcast Feeds",
|
||||||
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
|
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
|
||||||
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
|
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
|
||||||
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
|
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
"ButtonFullPath": "Ruta completa",
|
"ButtonFullPath": "Ruta completa",
|
||||||
"ButtonHide": "Ocultar",
|
"ButtonHide": "Ocultar",
|
||||||
"ButtonHome": "Inicio",
|
"ButtonHome": "Inicio",
|
||||||
"ButtonIssues": "Cuestiones",
|
"ButtonIssues": "Incidencias",
|
||||||
"ButtonJumpBackward": "Retroceder",
|
"ButtonJumpBackward": "Retroceder",
|
||||||
"ButtonJumpForward": "Adelantar",
|
"ButtonJumpForward": "Adelantar",
|
||||||
"ButtonLatest": "Más recientes",
|
"ButtonLatest": "Más recientes",
|
||||||
|
|
@ -850,7 +850,7 @@
|
||||||
"MessageNoEpisodes": "Ningún episodio",
|
"MessageNoEpisodes": "Ningún episodio",
|
||||||
"MessageNoFoldersAvailable": "Ninguna carpeta disponible",
|
"MessageNoFoldersAvailable": "Ninguna carpeta disponible",
|
||||||
"MessageNoGenres": "Ningún género",
|
"MessageNoGenres": "Ningún género",
|
||||||
"MessageNoIssues": "Ningún número",
|
"MessageNoIssues": "Sin incidencias",
|
||||||
"MessageNoItems": "Ningún elemento",
|
"MessageNoItems": "Ningún elemento",
|
||||||
"MessageNoItemsFound": "Ningún elemento encontrado",
|
"MessageNoItemsFound": "Ningún elemento encontrado",
|
||||||
"MessageNoListeningSessions": "Ninguna sesión de escucha",
|
"MessageNoListeningSessions": "Ninguna sesión de escucha",
|
||||||
|
|
@ -1116,8 +1116,8 @@
|
||||||
"ToastRemoveFailed": "Error al eliminar",
|
"ToastRemoveFailed": "Error al eliminar",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
|
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección",
|
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección",
|
||||||
"ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca incorrectos",
|
"ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca con incidencias",
|
||||||
"ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca incorrectos",
|
"ToastRemoveItemsWithIssuesSuccess": "Se eliminaron artículos de biblioteca con incidencias",
|
||||||
"ToastRenameFailed": "Error al cambiar el nombre",
|
"ToastRenameFailed": "Error al cambiar el nombre",
|
||||||
"ToastRescanFailed": "Error al volver a escanear para {0}",
|
"ToastRescanFailed": "Error al volver a escanear para {0}",
|
||||||
"ToastRescanRemoved": "Se eliminó el elemento reescaneado",
|
"ToastRescanRemoved": "Se eliminó el elemento reescaneado",
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
"ButtonEditChapters": "Modifica Capitoli",
|
"ButtonEditChapters": "Modifica Capitoli",
|
||||||
"ButtonEditPodcast": "Modifica Podcast",
|
"ButtonEditPodcast": "Modifica Podcast",
|
||||||
"ButtonEnable": "Abilita",
|
"ButtonEnable": "Abilita",
|
||||||
"ButtonFireAndFail": "Fire and Fail",
|
"ButtonFireAndFail": "Centro e fallimento",
|
||||||
"ButtonFireOnTest": "Fire onTest event",
|
"ButtonFireOnTest": "Fire onTest event",
|
||||||
"ButtonForceReScan": "Forza Re-Scan",
|
"ButtonForceReScan": "Forza Re-Scan",
|
||||||
"ButtonFullPath": "Percorso Completo",
|
"ButtonFullPath": "Percorso Completo",
|
||||||
|
|
@ -182,7 +182,7 @@
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Playlist",
|
||||||
"HeaderPlaylistItems": "Elementi della playlist",
|
"HeaderPlaylistItems": "Elementi della playlist",
|
||||||
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
|
||||||
"HeaderPresets": "Presets",
|
"HeaderPresets": "Preimpostazioni",
|
||||||
"HeaderPreviewCover": "Anteprima Cover",
|
"HeaderPreviewCover": "Anteprima Cover",
|
||||||
"HeaderRSSFeedGeneral": "Dettagli RSS",
|
"HeaderRSSFeedGeneral": "Dettagli RSS",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
|
||||||
|
|
@ -306,7 +306,7 @@
|
||||||
"LabelCustomCronExpression": "Espressione Cron personalizzata:",
|
"LabelCustomCronExpression": "Espressione Cron personalizzata:",
|
||||||
"LabelDatetime": "Data & Ora",
|
"LabelDatetime": "Data & Ora",
|
||||||
"LabelDays": "Giorni",
|
"LabelDays": "Giorni",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)",
|
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (despunta per rimuoverla solo dal database)",
|
||||||
"LabelDescription": "Descrizione",
|
"LabelDescription": "Descrizione",
|
||||||
"LabelDeselectAll": "Deseleziona Tutto",
|
"LabelDeselectAll": "Deseleziona Tutto",
|
||||||
"LabelDetectedPattern": "Trovato pattern:",
|
"LabelDetectedPattern": "Trovato pattern:",
|
||||||
|
|
@ -436,9 +436,9 @@
|
||||||
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
|
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
|
||||||
"LabelLibraryItem": "Elementi della biblioteca",
|
"LabelLibraryItem": "Elementi della biblioteca",
|
||||||
"LabelLibraryName": "Nome della biblioteca",
|
"LabelLibraryName": "Nome della biblioteca",
|
||||||
"LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti",
|
"LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento",
|
||||||
"LabelLibrarySortByProgressFinished": "Progressi: Completati",
|
"LabelLibrarySortByProgressFinished": "Progresso: finito",
|
||||||
"LabelLibrarySortByProgressStarted": "Progressi: Iniziati",
|
"LabelLibrarySortByProgressStarted": "Progresso: iniziato",
|
||||||
"LabelLimit": "Limiti",
|
"LabelLimit": "Limiti",
|
||||||
"LabelLineSpacing": "Interlinea",
|
"LabelLineSpacing": "Interlinea",
|
||||||
"LabelListenAgain": "Ascolta ancora",
|
"LabelListenAgain": "Ascolta ancora",
|
||||||
|
|
@ -497,7 +497,7 @@
|
||||||
"LabelNumberOfBooks": "Numero di libri",
|
"LabelNumberOfBooks": "Numero di libri",
|
||||||
"LabelNumberOfChapters": "Numero di capitoli:",
|
"LabelNumberOfChapters": "Numero di capitoli:",
|
||||||
"LabelNumberOfEpisodes": "Numero di episodi",
|
"LabelNumberOfEpisodes": "Numero di episodi",
|
||||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministratori (<b>se configurato</b>). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata come<code>falsa</code>. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:",
|
"LabelOpenIDAdvancedPermsClaimDescription": "Nome dell'attestazione OpenID che contiene autorizzazioni avanzate per le azioni dell'utente all'interno dell'applicazione che verranno applicate ai ruoli non amministrativi (<b>se configurato</b>). Se il reclamo manca nella risposta, l'accesso ad ABS verrà negato. Se manca una singola opzione, verrà trattata come <code>falso</code>. Assicurati che l'attestazione del provider di identità corrisponda alla struttura prevista:",
|
||||||
"LabelOpenIDClaims": "Lasciare vuote le seguenti opzioni per disabilitare l'assegnazione avanzata di gruppi e autorizzazioni, assegnando quindi automaticamente il gruppo \"Utente\".",
|
"LabelOpenIDClaims": "Lasciare vuote le seguenti opzioni per disabilitare l'assegnazione avanzata di gruppi e autorizzazioni, assegnando quindi automaticamente il gruppo \"Utente\".",
|
||||||
"LabelOpenIDGroupClaimDescription": "Nome dell'attestazione OpenID che contiene un elenco dei gruppi dell'utente. Comunemente indicato come <code>gruppo</code>. <b>se configurato</b>, l'applicazione assegnerà automaticamente i ruoli in base alle appartenenze ai gruppi dell'utente, a condizione che tali gruppi siano denominati \"admin\", \"utente\" o \"ospite\" senza distinzione tra maiuscole e minuscole nell'attestazione. L'attestazione deve contenere un elenco e, se un utente appartiene a più gruppi, l'applicazione assegnerà il ruolo corrispondente al livello di accesso più alto. Se nessun gruppo corrisponde, l'accesso verrà negato.",
|
"LabelOpenIDGroupClaimDescription": "Nome dell'attestazione OpenID che contiene un elenco dei gruppi dell'utente. Comunemente indicato come <code>gruppo</code>. <b>se configurato</b>, l'applicazione assegnerà automaticamente i ruoli in base alle appartenenze ai gruppi dell'utente, a condizione che tali gruppi siano denominati \"admin\", \"utente\" o \"ospite\" senza distinzione tra maiuscole e minuscole nell'attestazione. L'attestazione deve contenere un elenco e, se un utente appartiene a più gruppi, l'applicazione assegnerà il ruolo corrispondente al livello di accesso più alto. Se nessun gruppo corrisponde, l'accesso verrà negato.",
|
||||||
"LabelOpenRSSFeed": "Apri RSS Feed",
|
"LabelOpenRSSFeed": "Apri RSS Feed",
|
||||||
|
|
@ -530,7 +530,7 @@
|
||||||
"LabelPrimaryEbook": "Libro principale",
|
"LabelPrimaryEbook": "Libro principale",
|
||||||
"LabelProgress": "Cominciati",
|
"LabelProgress": "Cominciati",
|
||||||
"LabelProvider": "Fornitore",
|
"LabelProvider": "Fornitore",
|
||||||
"LabelProviderAuthorizationValue": "Authorization Header Value",
|
"LabelProviderAuthorizationValue": "Valore intestazione di autorizzazione",
|
||||||
"LabelPubDate": "Data di pubblicazione",
|
"LabelPubDate": "Data di pubblicazione",
|
||||||
"LabelPublishYear": "Anno di pubblicazione",
|
"LabelPublishYear": "Anno di pubblicazione",
|
||||||
"LabelPublishedDate": "Pubblicati {0}",
|
"LabelPublishedDate": "Pubblicati {0}",
|
||||||
|
|
@ -674,7 +674,7 @@
|
||||||
"LabelTimeDurationXMinutes": "{0} minuti",
|
"LabelTimeDurationXMinutes": "{0} minuti",
|
||||||
"LabelTimeDurationXSeconds": "{0} secondi",
|
"LabelTimeDurationXSeconds": "{0} secondi",
|
||||||
"LabelTimeInMinutes": "Tempo in minuti",
|
"LabelTimeInMinutes": "Tempo in minuti",
|
||||||
"LabelTimeLeft": "{0} sinistra",
|
"LabelTimeLeft": "{0} rimasti",
|
||||||
"LabelTimeListened": "Tempo di Ascolto",
|
"LabelTimeListened": "Tempo di Ascolto",
|
||||||
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
|
||||||
"LabelTimeRemaining": "{0} rimanente",
|
"LabelTimeRemaining": "{0} rimanente",
|
||||||
|
|
@ -682,7 +682,7 @@
|
||||||
"LabelTitle": "Titolo",
|
"LabelTitle": "Titolo",
|
||||||
"LabelToolsEmbedMetadata": "Incorpora Metadata",
|
"LabelToolsEmbedMetadata": "Incorpora Metadata",
|
||||||
"LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.",
|
"LabelToolsEmbedMetadataDescription": "Incorpora i metadati nei file audio, inclusi l'immagine di copertina e i capitoli.",
|
||||||
"LabelToolsM4bEncoder": "M4B Encoder",
|
"LabelToolsM4bEncoder": "Codificatore M4B",
|
||||||
"LabelToolsMakeM4b": "Crea un file M4B",
|
"LabelToolsMakeM4b": "Crea un file M4B",
|
||||||
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
|
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
|
||||||
"LabelToolsSplitM4b": "Converti M4B in MP3",
|
"LabelToolsSplitM4b": "Converti M4B in MP3",
|
||||||
|
|
@ -854,7 +854,7 @@
|
||||||
"MessageNoItems": "Nessun oggetto",
|
"MessageNoItems": "Nessun oggetto",
|
||||||
"MessageNoItemsFound": "Nessun oggetto trovato",
|
"MessageNoItemsFound": "Nessun oggetto trovato",
|
||||||
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
|
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
|
||||||
"MessageNoLogs": "Nessun Log",
|
"MessageNoLogs": "Nessun rapporto",
|
||||||
"MessageNoMediaProgress": "Nessun progresso multimediale",
|
"MessageNoMediaProgress": "Nessun progresso multimediale",
|
||||||
"MessageNoNotifications": "Nessuna notifica",
|
"MessageNoNotifications": "Nessuna notifica",
|
||||||
"MessageNoPodcastFeed": "Podcast non valido: nessun feed",
|
"MessageNoPodcastFeed": "Podcast non valido: nessun feed",
|
||||||
|
|
@ -1109,7 +1109,7 @@
|
||||||
"ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione",
|
"ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione",
|
||||||
"ToastProviderCreatedFailed": "Impossibile aggiungere il provider",
|
"ToastProviderCreatedFailed": "Impossibile aggiungere il provider",
|
||||||
"ToastProviderCreatedSuccess": "Aggiunto nuovo provider",
|
"ToastProviderCreatedSuccess": "Aggiunto nuovo provider",
|
||||||
"ToastProviderNameAndUrlRequired": "Nome e URL richiesti",
|
"ToastProviderNameAndUrlRequired": "Nome e Url richiesti",
|
||||||
"ToastProviderRemoveSuccess": "Provider rimosso",
|
"ToastProviderRemoveSuccess": "Provider rimosso",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
|
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
|
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,7 @@
|
||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
"LabelGenres": "Жанры",
|
"LabelGenres": "Жанры",
|
||||||
"LabelHardDeleteFile": "Жесткое удаление файла",
|
"LabelHardDeleteFile": "Жесткое удаление файла",
|
||||||
"LabelHasEbook": "Есть e-книга",
|
"LabelHasEbook": "Есть электронная книга",
|
||||||
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
|
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
|
||||||
"LabelHideSubtitles": "Скрыть серии",
|
"LabelHideSubtitles": "Скрыть серии",
|
||||||
"LabelHighestPriority": "Наивысший приоритет",
|
"LabelHighestPriority": "Наивысший приоритет",
|
||||||
|
|
@ -437,8 +437,8 @@
|
||||||
"LabelLibraryItem": "Элемент библиотеки",
|
"LabelLibraryItem": "Элемент библиотеки",
|
||||||
"LabelLibraryName": "Имя библиотеки",
|
"LabelLibraryName": "Имя библиотеки",
|
||||||
"LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
|
"LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
|
||||||
"LabelLibrarySortByProgressFinished": "Прогресс: Завершено",
|
"LabelLibrarySortByProgressFinished": "Прогресс: Закончена",
|
||||||
"LabelLibrarySortByProgressStarted": "Прогресс: Начато",
|
"LabelLibrarySortByProgressStarted": "Прогресс: Начата",
|
||||||
"LabelLimit": "Лимит",
|
"LabelLimit": "Лимит",
|
||||||
"LabelLineSpacing": "Межстрочный интервал",
|
"LabelLineSpacing": "Межстрочный интервал",
|
||||||
"LabelListenAgain": "Послушать снова",
|
"LabelListenAgain": "Послушать снова",
|
||||||
|
|
|
||||||
|
|
@ -275,7 +275,7 @@
|
||||||
"LabelBonus": "Bonus",
|
"LabelBonus": "Bonus",
|
||||||
"LabelBooks": "Knihy",
|
"LabelBooks": "Knihy",
|
||||||
"LabelButtonText": "Text tlačidla",
|
"LabelButtonText": "Text tlačidla",
|
||||||
"LabelByAuthor": "od",
|
"LabelByAuthor": "od {0}",
|
||||||
"LabelChangePassword": "Zmeniť heslo",
|
"LabelChangePassword": "Zmeniť heslo",
|
||||||
"LabelChannels": "Kanály",
|
"LabelChannels": "Kanály",
|
||||||
"LabelChapterCount": "{0} kapitol",
|
"LabelChapterCount": "{0} kapitol",
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.33.1",
|
"version": "2.33.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.33.1",
|
"version": "2.33.2",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.33.1",
|
"version": "2.33.2",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const CacheManager = require('../managers/CacheManager')
|
||||||
const CoverManager = require('../managers/CoverManager')
|
const CoverManager = require('../managers/CoverManager')
|
||||||
const AuthorFinder = require('../finders/AuthorFinder')
|
const AuthorFinder = require('../finders/AuthorFinder')
|
||||||
|
|
||||||
const { reqSupportsWebp, isValidASIN } = require('../utils/index')
|
const { reqSupportsWebp, isValidASIN, clampPositiveInt } = require('../utils/index')
|
||||||
|
|
||||||
const naturalSort = createNewSortInstance({
|
const naturalSort = createNewSortInstance({
|
||||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||||
|
|
@ -412,8 +412,8 @@ class AuthorController {
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||||
height: height ? parseInt(height) : null,
|
height: clampPositiveInt(height ? parseInt(height) : null, 4096),
|
||||||
width: width ? parseInt(width) : null
|
width: clampPositiveInt(width ? parseInt(width) : null, 4096)
|
||||||
}
|
}
|
||||||
return CacheManager.handleAuthorCache(res, authorId, options)
|
return CacheManager.handleAuthorCache(res, authorId, options)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class FileSystemController {
|
||||||
filepath = fileUtils.filePathToPOSIX(filepath)
|
filepath = fileUtils.filePathToPOSIX(filepath)
|
||||||
|
|
||||||
// Ensure filepath is inside library folder (prevents directory traversal)
|
// Ensure filepath is inside library folder (prevents directory traversal)
|
||||||
if (!filepath.startsWith(libraryFolder.path)) {
|
if (!fileUtils.isSameOrSubPath(libraryFolder.path, filepath)) {
|
||||||
Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`)
|
Logger.error(`[FileSystemController] Filepath is not inside library folder: ${filepath}`)
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -462,7 +462,7 @@ class LibraryController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`)
|
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from folder "${folder.path}"`)
|
||||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorIds.length) {
|
if (authorIds.length) {
|
||||||
|
|
@ -563,7 +563,7 @@ class LibraryController {
|
||||||
mediaItemIds.push(libraryItem.mediaId)
|
mediaItemIds.push(libraryItem.mediaId)
|
||||||
}
|
}
|
||||||
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`)
|
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" from library "${req.library.name}"`)
|
||||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set PlaybackSessions libraryId to null
|
// Set PlaybackSessions libraryId to null
|
||||||
|
|
@ -714,7 +714,7 @@ class LibraryController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" with issue`)
|
Logger.info(`[LibraryController] Removing library item "${libraryItem.id}" with issue`)
|
||||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, req.library.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authorIds.length) {
|
if (authorIds.length) {
|
||||||
|
|
@ -1435,10 +1435,15 @@ class LibraryController {
|
||||||
const libraryItems = await Database.libraryItemModel.findAll({
|
const libraryItems = await Database.libraryItemModel.findAll({
|
||||||
attributes: ['id', 'libraryId', 'path', 'isFile'],
|
attributes: ['id', 'libraryId', 'path', 'isFile'],
|
||||||
where: {
|
where: {
|
||||||
id: itemIds
|
id: itemIds,
|
||||||
|
libraryId: req.library.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (libraryItems.length < itemIds.length) {
|
||||||
|
Logger.warn(`[LibraryController] User "${req.user.username}" requested ${itemIds.length} items but only ${libraryItems.length} are in library "${req.library.id}"`)
|
||||||
|
}
|
||||||
|
|
||||||
Logger.info(`[LibraryController] User "${req.user.username}" requested download for items "${itemIds}"`)
|
Logger.info(`[LibraryController] User "${req.user.username}" requested download for items "${itemIds}"`)
|
||||||
|
|
||||||
const filename = `LibraryItems-${Date.now()}.zip`
|
const filename = `LibraryItems-${Date.now()}.zip`
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const SocketAuthority = require('../SocketAuthority')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
|
|
||||||
const zipHelpers = require('../utils/zipHelpers')
|
const zipHelpers = require('../utils/zipHelpers')
|
||||||
const { reqSupportsWebp } = require('../utils/index')
|
const { reqSupportsWebp, clampPositiveInt } = require('../utils/index')
|
||||||
const { ScanResult, AudioMimeType } = require('../utils/constants')
|
const { ScanResult, AudioMimeType } = require('../utils/constants')
|
||||||
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
|
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
|
||||||
const LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
const LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
||||||
|
|
@ -111,7 +111,7 @@ class LibraryItemController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds)
|
await this.handleDeleteLibraryItem(req.libraryItem.id, mediaItemIds, req.libraryItem.libraryId)
|
||||||
if (hardDelete) {
|
if (hardDelete) {
|
||||||
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
||||||
await fs.remove(libraryItemPath).catch((error) => {
|
await fs.remove(libraryItemPath).catch((error) => {
|
||||||
|
|
@ -398,8 +398,8 @@ class LibraryItemController {
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||||
height: height ? parseInt(height) : null,
|
height: clampPositiveInt(height ? parseInt(height) : null, 4096),
|
||||||
width: width ? parseInt(width) : null
|
width: clampPositiveInt(width ? parseInt(width) : null, 4096)
|
||||||
}
|
}
|
||||||
return CacheManager.handleCoverCache(res, libraryItemId, options)
|
return CacheManager.handleCoverCache(res, libraryItemId, options)
|
||||||
}
|
}
|
||||||
|
|
@ -565,7 +565,7 @@ class LibraryItemController {
|
||||||
authorIds.push(...libraryItem.media.authors.map((au) => au.id))
|
authorIds.push(...libraryItem.media.authors.map((au) => au.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds)
|
await this.handleDeleteLibraryItem(libraryItem.id, mediaItemIds, libraryItem.libraryId)
|
||||||
if (hardDelete) {
|
if (hardDelete) {
|
||||||
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
|
||||||
await fs.remove(libraryItemPath).catch((error) => {
|
await fs.remove(libraryItemPath).catch((error) => {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const Database = require('../Database')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
||||||
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils')
|
const { getFileTimestampsWithIno, filePathToPOSIX, isSameOrSubPath } = require('../utils/fileUtils')
|
||||||
const { validateUrl } = require('../utils/index')
|
const { validateUrl } = require('../utils/index')
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
|
|
||||||
|
|
@ -58,8 +58,18 @@ class PodcastController {
|
||||||
return res.status(404).send('Folder not found')
|
return res.status(404).send('Folder not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof payload.path !== 'string' || !payload.path.trim()) {
|
||||||
|
return res.status(400).send('Invalid request body. "path" must be a non-empty string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryFolderPath = filePathToPOSIX(folder.path)
|
||||||
const podcastPath = filePathToPOSIX(payload.path)
|
const podcastPath = filePathToPOSIX(payload.path)
|
||||||
|
|
||||||
|
if (!isSameOrSubPath(libraryFolderPath, podcastPath)) {
|
||||||
|
Logger.error(`[PodcastController] Create: Podcast path is outside library folder "${libraryFolderPath}": "${podcastPath}"`)
|
||||||
|
return res.status(400).send('Podcast path must be inside the selected library folder')
|
||||||
|
}
|
||||||
|
|
||||||
// Check if a library item with this podcast folder exists already
|
// Check if a library item with this podcast folder exists already
|
||||||
const existingLibraryItem =
|
const existingLibraryItem =
|
||||||
(await Database.libraryItemModel.count({
|
(await Database.libraryItemModel.count({
|
||||||
|
|
@ -83,7 +93,7 @@ class PodcastController {
|
||||||
|
|
||||||
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||||
|
|
||||||
let relPath = payload.path.replace(folder.fullPath, '')
|
let relPath = podcastPath.replace(libraryFolderPath, '')
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||||
|
|
||||||
let newLibraryItem = null
|
let newLibraryItem = null
|
||||||
|
|
|
||||||
|
|
@ -126,13 +126,31 @@ class BackupManager {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Not a valid zip file
|
// Not a valid zip file
|
||||||
Logger.error('[BackupManager] Failed to read backup file - backup might not be a valid .zip file', tempPath, error)
|
Logger.error('[BackupManager] Failed to read backup file - backup might not be a valid .zip file', tempPath, error)
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err))
|
||||||
return res.status(400).send('Failed to read backup file - backup might not be a valid .zip file')
|
return res.status(400).send('Failed to read backup file - backup might not be a valid .zip file')
|
||||||
}
|
}
|
||||||
if (!Object.keys(entries).includes('absdatabase.sqlite')) {
|
if (!entries['absdatabase.sqlite']) {
|
||||||
Logger.error(`[BackupManager] Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.`)
|
Logger.error(`[BackupManager] Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.`)
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err))
|
||||||
return res.status(500).send('Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.')
|
return res.status(500).send('Invalid backup with no absdatabase.sqlite file - might be a backup created on an old Audiobookshelf server.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detailsEntry = entries['details']
|
||||||
|
if (!detailsEntry) {
|
||||||
|
Logger.error('[BackupManager] Invalid backup - missing details entry')
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err))
|
||||||
|
return res.status(400).send('Invalid backup file - missing details entry')
|
||||||
|
}
|
||||||
|
if (detailsEntry.size > 1024 * 1024) {
|
||||||
|
Logger.error(`[BackupManager] Backup details entry too large: ${detailsEntry.size} bytes`)
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err))
|
||||||
|
return res.status(400).send('Invalid backup file - details entry too large')
|
||||||
|
}
|
||||||
|
|
||||||
const data = await zip.entryData('details')
|
const data = await zip.entryData('details')
|
||||||
const details = data.toString('utf8').split('\n')
|
const details = data.toString('utf8').split('\n')
|
||||||
|
|
||||||
|
|
@ -140,9 +158,13 @@ class BackupManager {
|
||||||
|
|
||||||
if (!backup.serverVersion) {
|
if (!backup.serverVersion) {
|
||||||
Logger.error(`[BackupManager] Invalid backup with no server version - might be a backup created before version 2.0.0`)
|
Logger.error(`[BackupManager] Invalid backup with no server version - might be a backup created before version 2.0.0`)
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
await fs.remove(tempPath).catch((err) => Logger.error(`[BackupManager] Failed to remove rejected backup file "${tempPath}"`, err))
|
||||||
return res.status(500).send('Invalid backup. Might be a backup created before version 2.0.0.')
|
return res.status(500).send('Invalid backup. Might be a backup created before version 2.0.0.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
|
||||||
backup.fileSize = await getFileSize(backup.fullPath)
|
backup.fileSize = await getFileSize(backup.fullPath)
|
||||||
|
|
||||||
const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id)
|
const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id)
|
||||||
|
|
@ -257,9 +279,24 @@ class BackupManager {
|
||||||
let data = null
|
let data = null
|
||||||
try {
|
try {
|
||||||
zip = new StreamZip.async({ file: fullFilePath })
|
zip = new StreamZip.async({ file: fullFilePath })
|
||||||
|
const entries = await zip.entries()
|
||||||
|
|
||||||
|
const detailsEntry = entries['details']
|
||||||
|
if (!detailsEntry) {
|
||||||
|
Logger.error(`[BackupManager] Backup "${fullFilePath}" missing details entry - skipping`)
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (detailsEntry.size > 1024 * 1024) {
|
||||||
|
Logger.error(`[BackupManager] Backup "${fullFilePath}" details entry too large (${detailsEntry.size} bytes) - skipping`)
|
||||||
|
await zip.close().catch(() => {})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
data = await zip.entryData('details')
|
data = await zip.entryData('details')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error)
|
Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error)
|
||||||
|
if (zip) await zip.close().catch(() => {})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,16 +111,17 @@ class Author extends Model {
|
||||||
*
|
*
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
* @returns {Promise<Author>}
|
* @returns {Promise<{ author: Author, created: boolean }>}
|
||||||
*/
|
*/
|
||||||
static async findOrCreateByNameAndLibrary(name, libraryId) {
|
static async findOrCreateByNameAndLibrary(name, libraryId) {
|
||||||
const author = await this.getByNameAndLibrary(name, libraryId)
|
const author = await this.getByNameAndLibrary(name, libraryId)
|
||||||
if (author) return author
|
if (author) return { author, created: false }
|
||||||
return this.create({
|
const newAuthor = await this.create({
|
||||||
name,
|
name,
|
||||||
lastFirst: this.getLastFirst(name),
|
lastFirst: this.getLastFirst(name),
|
||||||
libraryId
|
libraryId
|
||||||
})
|
})
|
||||||
|
return { author: newAuthor, created: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
|
||||||
const parseNameString = require('../utils/parsers/parseNameString')
|
const parseNameString = require('../utils/parsers/parseNameString')
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef EBookFileObject
|
* @typedef EBookFileObject
|
||||||
|
|
@ -470,13 +471,23 @@ class Book extends Model {
|
||||||
|
|
||||||
for (const author of authorsRemoved) {
|
for (const author of authorsRemoved) {
|
||||||
await bookAuthorModel.removeByIds(author.id, this.id)
|
await bookAuthorModel.removeByIds(author.id, this.id)
|
||||||
|
const numBooks = await bookAuthorModel.getCountForAuthor(author.id)
|
||||||
|
if (numBooks > 0) {
|
||||||
|
SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks))
|
||||||
|
}
|
||||||
Logger.debug(`[Book] "${this.title}" Removed author "${author.name}"`)
|
Logger.debug(`[Book] "${this.title}" Removed author "${author.name}"`)
|
||||||
this.authors = this.authors.filter((au) => au.id !== author.id)
|
this.authors = this.authors.filter((au) => au.id !== author.id)
|
||||||
}
|
}
|
||||||
const authorsAdded = []
|
const authorsAdded = []
|
||||||
for (const authorName of newAuthorNames) {
|
for (const authorName of newAuthorNames) {
|
||||||
const author = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId)
|
const { author, created } = await authorModel.findOrCreateByNameAndLibrary(authorName, libraryId)
|
||||||
await bookAuthorModel.create({ bookId: this.id, authorId: author.id })
|
await bookAuthorModel.create({ bookId: this.id, authorId: author.id })
|
||||||
|
if (created) {
|
||||||
|
SocketAuthority.emitter('author_added', author.toOldJSON())
|
||||||
|
} else {
|
||||||
|
const numBooks = await bookAuthorModel.getCountForAuthor(author.id)
|
||||||
|
SocketAuthority.emitter('author_updated', author.toOldJSONExpanded(numBooks))
|
||||||
|
}
|
||||||
Logger.debug(`[Book] "${this.title}" Added author "${author.name}"`)
|
Logger.debug(`[Book] "${this.title}" Added author "${author.name}"`)
|
||||||
this.authors.push(author)
|
this.authors.push(author)
|
||||||
authorsAdded.push(author)
|
authorsAdded.push(author)
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,8 @@ class PlaybackSession {
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }),
|
audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }),
|
||||||
libraryItem: libraryItem?.toOldJSONExpanded() || null
|
libraryItem: libraryItem?.toOldJSONExpanded() || null,
|
||||||
|
coverAspectRatio: this.coverAspectRatio !== null ? this.coverAspectRatio : undefined // Used for share sessions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class Stream extends EventEmitter {
|
||||||
return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF]
|
return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF]
|
||||||
}
|
}
|
||||||
get codecsToForceAAC() {
|
get codecsToForceAAC() {
|
||||||
return ['alac', 'ac3', 'eac3']
|
return ['alac', 'ac3', 'eac3', 'opus']
|
||||||
}
|
}
|
||||||
get userToken() {
|
get userToken() {
|
||||||
return this.user.token
|
return this.user.token
|
||||||
|
|
|
||||||
|
|
@ -363,8 +363,9 @@ class ApiRouter {
|
||||||
* Remove library item and associated entities
|
* Remove library item and associated entities
|
||||||
* @param {string} libraryItemId
|
* @param {string} libraryItemId
|
||||||
* @param {string[]} mediaItemIds array of bookId or podcastEpisodeId
|
* @param {string[]} mediaItemIds array of bookId or podcastEpisodeId
|
||||||
|
* @param {string} libraryId
|
||||||
*/
|
*/
|
||||||
async handleDeleteLibraryItem(libraryItemId, mediaItemIds) {
|
async handleDeleteLibraryItem(libraryItemId, mediaItemIds, libraryId) {
|
||||||
const numProgressRemoved = await Database.mediaProgressModel.destroy({
|
const numProgressRemoved = await Database.mediaProgressModel.destroy({
|
||||||
where: {
|
where: {
|
||||||
mediaItemId: mediaItemIds
|
mediaItemId: mediaItemIds
|
||||||
|
|
@ -395,7 +396,8 @@ class ApiRouter {
|
||||||
await Database.libraryItemModel.removeById(libraryItemId)
|
await Database.libraryItemModel.removeById(libraryItemId)
|
||||||
|
|
||||||
SocketAuthority.emitter('item_removed', {
|
SocketAuthority.emitter('item_removed', {
|
||||||
id: libraryItemId
|
id: libraryItemId,
|
||||||
|
libraryId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ module.exports.AudioMimeType = {
|
||||||
AIF: 'audio/x-aiff',
|
AIF: 'audio/x-aiff',
|
||||||
WEBM: 'audio/webm',
|
WEBM: 'audio/webm',
|
||||||
WEBMA: 'audio/webm',
|
WEBMA: 'audio/webm',
|
||||||
|
// TODO: Switch to `audio/matroska`? marked as deprecated in IANA registry
|
||||||
|
// ref: https://datatracker.ietf.org/doc/html/rfc9559
|
||||||
MKA: 'audio/x-matroska',
|
MKA: 'audio/x-matroska',
|
||||||
AWB: 'audio/amr-wb',
|
AWB: 'audio/amr-wb',
|
||||||
CAF: 'audio/x-caf',
|
CAF: 'audio/x-caf',
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,16 @@ module.exports.isNullOrNaN = (num) => {
|
||||||
return num === null || isNaN(num)
|
return num === null || isNaN(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number|null|undefined} value
|
||||||
|
* @param {number} max
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
module.exports.clampPositiveInt = (value, max) => {
|
||||||
|
if (value == null || !Number.isFinite(value) || value <= 0) return null
|
||||||
|
return Math.min(Math.floor(value), max)
|
||||||
|
}
|
||||||
|
|
||||||
const xmlToJSON = (xml) => {
|
const xmlToJSON = (xml) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
parseString(xml, (err, results) => {
|
parseString(xml, (err, results) => {
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,10 @@ function extractEpisodeData(item) {
|
||||||
episode[cleanKey] = extractFirstArrayItemString(item, key)
|
episode[cleanKey] = extractFirstArrayItemString(item, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (episode.subtitle) {
|
||||||
|
episode.subtitle = htmlSanitizer.sanitize(episode.subtitle.trim())
|
||||||
|
}
|
||||||
|
|
||||||
// Extract psc:chapters if duration is set
|
// Extract psc:chapters if duration is set
|
||||||
episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null
|
episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue