Compare commits

..

31 Commits

Author SHA1 Message Date
advplyr b41db23994 Version bump v2.33.2 2026-04-19 16:46:10 -05:00
advplyr 125f265f55 Merge pull request #5141 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-04-19 16:21:40 -05:00
Gernomaly aa4a191567 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:46 +00:00
Jan e431ea0472 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:45 +00:00
Laurin Sorgend e3388d4446 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:45 +00:00
Mario 88879f1409 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:44 +00:00
Pavel Miniutka 3e0099e8d9 Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-04-19 21:20:44 +00:00
tizio04 f558182d94 Translated using Weblate (Italian)
Currently translated at 99.9% (1162 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2026-04-19 21:20:43 +00:00
Владимир Макеев a30fe15b10 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2026-04-19 21:20:43 +00:00
A L 0bbf8bde5c Translated using Weblate (Bulgarian)
Currently translated at 91.2% (1061 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2026-04-19 21:20:42 +00:00
Alex 0e2cdde731 Translated using Weblate (Slovak)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2026-04-19 21:20:41 +00:00
tfr tint bc6bfbe804 Translated using Weblate (Italian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2026-04-19 21:20:41 +00:00
Vadzim Kurdzesau 2755204168 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2026-04-19 21:20:40 +00:00
Francisco Serrador 2d4df273f0 Translated using Weblate (Spanish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-04-19 21:20:40 +00:00
Pavel Miniutka d73b64a19c Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-04-19 21:20:39 +00:00
advplyr b7e8a0474a Update bulk download endpoint ensure items are from the same library requested 2026-04-19 16:20:31 -05:00
advplyr 39adefb632 Update backup load & upload to remove tempfile on failed backups, validate details filesize & close zip 2026-04-18 17:03:37 -05:00
advplyr 24cab79c66 Update filesystem/pathexists endpoint to use existing isSameOrSubPath func 2026-04-18 16:24:48 -05:00
advplyr b27f21fd95 Update podcastUtils to sanitize episode subtitle from rss feed 2026-04-17 16:59:22 -05:00
advplyr 09fa0b38f5 Update podcast create path validation & fix relPath 2026-04-17 16:51:22 -05:00
advplyr 455e605162 Update author & library item image endpoints to clamp width/height query params 2026-04-17 16:30:08 -05:00
advplyr 88667d00a1 Merge pull request #5115 from rktjmp/fix-mka-opus-desktop-streaming
Force AAC transcode when streaming mka+opus to desktop client
2026-04-10 16:54:18 -05:00
advplyr 94c426bd97 Update comments on matroska 2026-04-10 16:42:39 -05:00
Oliver Marriott 522b9735e2 Add audio/(x-)matroska to client player MIME types to avoid transcode
Firefox, at least, supports playing `matroska/audio` containers natively but
the client was not checking for support. Clients  that do not support
playing `matroska/audio` containers will fallback to transcoding.
2026-04-09 21:07:14 +10:00
advplyr 64cbf59609 Merge pull request #5160 from mikiher/fix-item-removed-payload
Fix item_removed payload to include libraryId
2026-03-31 16:43:51 -05:00
mikiher fda1a6ea9b Fix item_removed payload to include libraryId 2026-03-31 22:02:52 +03:00
advplyr c4c8b8d0f2 Merge pull request #5158 from mikiher/book-update-author-events
Emit proper author_updated/added events when updating book media
2026-03-30 16:26:47 -05:00
advplyr ab3bd6f4a1 Update JS docs 2026-03-30 16:22:27 -05:00
mikiher 093124aac6 Emit proper author_updated/added events when updating book media 2026-03-30 22:02:56 +03:00
advplyr 5de92d08f9 Fix share playback session not including coverAspectRatio 2026-03-29 15:36:07 -05:00
Oliver Marriott d9355ac3aa Force AAC transcode when streaming mka+opus to desktop client
Matroska audio containers (aka mka files) with Opus codec streams inside
were unplayable on the desktop client because hls.js was unable to
decode the stream, resulting in an infinitely "spinning" play button.

When configuring a stream, we now check for the opus codec and force AAC
transcoding.

Matroska containers support other codecs besides Opus, eg: mp3, which do
not require transcoding and work fine before this patch, which is why we
check for opus in codecsToForceAAC instead of AudioMimeType.MKA in
mimeTypesToForceAAC.

The AudioMimeType.OPUS mimetype is already marked as requiring
transcoding but since its inside a container this check does not
evaluate to true, we must check the codec explicitly.
2026-03-11 00:35:12 +11:00
26 changed files with 197 additions and 67 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.33.1",
"version": "2.33.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.33.1",
"version": "2.33.2",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.33.1",
"version": "2.33.2",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
+14 -1
View File
@@ -46,7 +46,20 @@ export default class LocalAudioPlayer extends EventEmitter {
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.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 = {}
mimeTypes.forEach((mt) => {
var canPlay = this.player.canPlayType(mt)
+9 -9
View File
@@ -16,7 +16,7 @@
"ButtonBrowseForFolder": "Агляд папак",
"ButtonCancel": "Скасаваць",
"ButtonCancelEncode": "Скасаваць кадзіраванне",
"ButtonChangeRootPassword": "Зменіце Root пароль",
"ButtonChangeRootPassword": "Змяніць пароль root",
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя выпускі",
"ButtonChooseAFolder": "Выбраць папку",
"ButtonChooseFiles": "Выбраць файлы",
@@ -81,7 +81,7 @@
"ButtonRemove": "Выдаліць",
"ButtonRemoveAll": "Выдаліць усе",
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
"ButtonRemoveFromContinueListening": "Выдаліць з Працягнуць праслухоўванне",
"ButtonRemoveFromContinueListening": "Выдаліць з Працяг праслухоўвання",
"ButtonRemoveFromContinueReading": "Выдаліць з Працягваць чытанне",
"ButtonRemoveSeriesFromContinueSeries": "Выдаліць серыю з Працягваць серыю",
"ButtonReset": "Скінуць",
@@ -252,8 +252,8 @@
"LabelAudioChannels": "Аўдыяканалы (1 або 2)",
"LabelAudioCodec": "Аўдыякодэк",
"LabelAuthor": "Аўтар",
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)",
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
"LabelAuthorFirstLast": "Аўтар (імя, прозвішча)",
"LabelAuthorLastFirst": "Аўтар (прозвішча, імя)",
"LabelAuthors": "Аўтары",
"LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі",
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метаданых",
@@ -292,7 +292,7 @@
"LabelCollections": "Калекцыі",
"LabelComplete": "Завяршыць",
"LabelConfirmPassword": "Пацвердзіце пароль",
"LabelContinueListening": "Працягнуць праслухоўванне",
"LabelContinueListening": "Працяг праслухоўвання",
"LabelContinueReading": "Працягнуць чытанне",
"LabelContinueSeries": "Працягнуць серыі",
"LabelCorsAllowed": "Дазволеныя крыніцы CORS",
@@ -424,7 +424,7 @@
"LabelLastBookAdded": "Апошняя дададзеная кніга",
"LabelLastBookUpdated": "Апошняя абноўленая кніга",
"LabelLastProgressDate": "Апошні прагрэс: {0}",
"LabelLastSeen": "Апошні прагляд",
"LabelLastSeen": "Апошняя актыўнасць",
"LabelLastTime": "Апошні раз",
"LabelLastUpdate": "Апошняе абнаўленне",
"LabelLayout": "Знешні выгляд",
@@ -545,7 +545,7 @@
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
"LabelRSSFeedURL": "URL RSS-стужкі",
"LabelRandomly": "Выпадкова",
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягнуць праслухоўванне",
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працяг праслухоўвання",
"LabelRead": "Чытаць",
"LabelReadAgain": "Чытаць зноў",
"LabelReadEbookWithoutProgress": "Чытаць электронную кнігу без захавання прагрэсу",
@@ -634,12 +634,12 @@
"LabelSortAscending": "Па ўзрастанні",
"LabelSortDescending": "Па ўбыванні",
"LabelSortPubDate": "Сартаваць па даце публікацыі",
"LabelStart": "Пачаць",
"LabelStart": "Пачатак",
"LabelStartTime": "Час пачатку",
"LabelStarted": "Пачата",
"LabelStartedAt": "Пачата ў",
"LabelStartedDate": "Пачата {0}",
"LabelStatsAudioTracks": "Аўдыятрэкаў",
"LabelStatsAudioTracks": "Аўдыятрэкі",
"LabelStatsAuthors": "Аўтараў",
"LabelStatsBestDay": "Найлепшы дзень",
"LabelStatsDailyAverage": "У сярэднім за дзень",
+36 -2
View File
@@ -436,7 +436,7 @@
"LabelLibraryFilterSublistEmpty": "Не {0}",
"LabelLibraryItem": "Елемент на Библиотека",
"LabelLibraryName": "Име на Библиотека",
"LabelLibrarySortByProgress": "Прогрес: Последно Обновен",
"LabelLibrarySortByProgress": "Прогрес: Последно обновление",
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
"LabelLimit": "Лимит",
@@ -892,7 +892,7 @@
"MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}",
"MessageSearchResultsFor": "Резултати от търсенето за",
"MessageSelected": "{0} избрани",
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации.",
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации",
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
"MessageShareExpirationWillBe": "Изтичането ще бъде на <strong>{0}</strong>",
@@ -956,6 +956,8 @@
"NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод",
"NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити",
"NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод",
"NotificationOnTestDescription": "Event за тестване на системата за нотификации",
"PlaceholderBulkChapterInput": "Въведете име на глава или използвайте номериране (прим. 'Епизод 1', 'Глава 10', '1.')",
"PlaceholderNewCollection": "Ново име на колекцията",
"PlaceholderNewFolderPath": "Нов път на папката",
"PlaceholderNewPlaylist": "Ново име на плейлиста",
@@ -963,26 +965,58 @@
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
"StatsAuthorsAdded": "добаврени автори",
"StatsBooksAdded": "добавени книги",
"StatsBooksAdditional": "Някой от вкючените добавки…",
"StatsBooksFinished": "завършени книги",
"StatsBooksFinishedThisYear": "Някой от книгите приключени тази година…",
"StatsBooksListenedTo": "слушани книги",
"StatsCollectionGrewTo": "Твоята книжна колекция израсна до…",
"StatsSessions": "сесии",
"StatsSpentListening": "прекарано в слушане",
"StatsTopAuthor": "ТОП АВТОР",
"StatsTopAuthors": "ТОП АВТОРИ",
"StatsTopGenre": "ТОП ЖАНР",
"StatsTopGenres": "ТОП ЖАНРА",
"StatsTopMonth": "ТОП МЕСЕЦ",
"StatsTopNarrator": "ТОП РАЗКАЗВАЧ",
"StatsTopNarrators": "ТОП РАЗКАЗВАЧИ",
"StatsTotalDuration": "С пълно времетраене…",
"StatsYearInReview": "ГОДИНАТА В ПРЕГЛЕД",
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
"ToastAppriseUrlRequired": "Трябва да въведете Apprise URL",
"ToastAsinRequired": "ASIN-а е задължителен",
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
"ToastAuthorNotFound": "Автор \"{0}\" не е намерен",
"ToastAuthorRemoveSuccess": "Арторът е премахнат",
"ToastAuthorSearchNotFound": "Авторът не е намерен",
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
"ToastAuthorUpdateSuccess": "Автора обновен",
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
"ToastBackupAppliedSuccess": "Архивът е приложен",
"ToastBackupCreateFailed": "Неуспешно създаване на архив",
"ToastBackupCreateSuccess": "Архивът е създаден",
"ToastBackupDeleteFailed": "Неуспешно изтриване на архив",
"ToastBackupDeleteSuccess": "Архивът е изтрит",
"ToastBackupInvalidMaxKeep": "Невалиден брой за архиви за запазване",
"ToastBackupInvalidMaxSize": "Невалиден максимален рамер на архив",
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
"ToastBackupUploadFailed": "Неуспешно качване на архив",
"ToastBackupUploadSuccess": "Архивът е качен",
"ToastBatchApplyDetailsToItemsSuccess": "Детайли приложени на предмети",
"ToastBatchDeleteFailed": "Груповото изтриване се провали",
"ToastBatchDeleteSuccess": "Успешно групово изтриване",
"ToastBatchQuickMatchFailed": "Груповото Бързо Съвпадение се провали!",
"ToastBatchQuickMatchStarted": "Груповото Бързо Съвпадение на {0} книги започна!",
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена",
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
"ToastBulkChapterInvalidCount": "Въведете число между 1 и 150",
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
"ToastChapterLocked": "Главата е заключена.",
"ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди",
"ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.",
"ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
+5 -5
View File
@@ -116,7 +116,7 @@
"ButtonViewAll": "Alles anzeigen",
"ButtonYes": "Ja",
"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",
"HeaderAccount": "Konto",
"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",
"LabelSettingsTimeFormat": "Zeitformat",
"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",
"LabelShareURL": "Freigabe URL",
"LabelShowAll": "Alles anzeigen",
@@ -737,7 +737,7 @@
"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.",
"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.",
"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.",
@@ -816,7 +816,7 @@
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
"MessageFetching": "Wird abgerufen …",
"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}",
"MessageImportantNotice": "Wichtiger Hinweis!",
"MessageInsertChapterBelow": "Kapitel unten einfügen",
@@ -1103,7 +1103,7 @@
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
"ToastPodcastCreateSuccess": "Podcast erstellt",
"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",
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
+4 -4
View File
@@ -40,7 +40,7 @@
"ButtonFullPath": "Ruta completa",
"ButtonHide": "Ocultar",
"ButtonHome": "Inicio",
"ButtonIssues": "Cuestiones",
"ButtonIssues": "Incidencias",
"ButtonJumpBackward": "Retroceder",
"ButtonJumpForward": "Adelantar",
"ButtonLatest": "Más recientes",
@@ -850,7 +850,7 @@
"MessageNoEpisodes": "Ningún episodio",
"MessageNoFoldersAvailable": "Ninguna carpeta disponible",
"MessageNoGenres": "Ningún género",
"MessageNoIssues": "Ningún número",
"MessageNoIssues": "Sin incidencias",
"MessageNoItems": "Ningún elemento",
"MessageNoItemsFound": "Ningún elemento encontrado",
"MessageNoListeningSessions": "Ninguna sesión de escucha",
@@ -1116,8 +1116,8 @@
"ToastRemoveFailed": "Error al eliminar",
"ToastRemoveItemFromCollectionFailed": "Error al eliminar el elemento de la colección",
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección",
"ToastRemoveItemsWithIssuesFailed": "Error en la eliminación de artículos de biblioteca incorrectos",
"ToastRemoveItemsWithIssuesSuccess": "Se eliminaron 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 con incidencias",
"ToastRenameFailed": "Error al cambiar el nombre",
"ToastRescanFailed": "Error al volver a escanear para {0}",
"ToastRescanRemoved": "Se eliminó el elemento reescaneado",
+12 -12
View File
@@ -34,7 +34,7 @@
"ButtonEditChapters": "Modifica Capitoli",
"ButtonEditPodcast": "Modifica Podcast",
"ButtonEnable": "Abilita",
"ButtonFireAndFail": "Fire and Fail",
"ButtonFireAndFail": "Centro e fallimento",
"ButtonFireOnTest": "Fire onTest event",
"ButtonForceReScan": "Forza Re-Scan",
"ButtonFullPath": "Percorso Completo",
@@ -182,7 +182,7 @@
"HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Elementi della playlist",
"HeaderPodcastsToAdd": "Podcasts da Aggiungere",
"HeaderPresets": "Presets",
"HeaderPresets": "Preimpostazioni",
"HeaderPreviewCover": "Anteprima Cover",
"HeaderRSSFeedGeneral": "Dettagli RSS",
"HeaderRSSFeedIsOpen": "RSS Feed è aperto",
@@ -306,7 +306,7 @@
"LabelCustomCronExpression": "Espressione Cron personalizzata:",
"LabelDatetime": "Data & Ora",
"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",
"LabelDeselectAll": "Deseleziona Tutto",
"LabelDetectedPattern": "Trovato pattern:",
@@ -436,9 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
"LabelLibraryItem": "Elementi della biblioteca",
"LabelLibraryName": "Nome della biblioteca",
"LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti",
"LabelLibrarySortByProgressFinished": "Progressi: Completati",
"LabelLibrarySortByProgressStarted": "Progressi: Iniziati",
"LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento",
"LabelLibrarySortByProgressFinished": "Progresso: finito",
"LabelLibrarySortByProgressStarted": "Progresso: iniziato",
"LabelLimit": "Limiti",
"LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Ascolta ancora",
@@ -497,7 +497,7 @@
"LabelNumberOfBooks": "Numero di libri",
"LabelNumberOfChapters": "Numero di capitoli:",
"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\".",
"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",
@@ -530,7 +530,7 @@
"LabelPrimaryEbook": "Libro principale",
"LabelProgress": "Cominciati",
"LabelProvider": "Fornitore",
"LabelProviderAuthorizationValue": "Authorization Header Value",
"LabelProviderAuthorizationValue": "Valore intestazione di autorizzazione",
"LabelPubDate": "Data di pubblicazione",
"LabelPublishYear": "Anno di pubblicazione",
"LabelPublishedDate": "Pubblicati {0}",
@@ -674,7 +674,7 @@
"LabelTimeDurationXMinutes": "{0} minuti",
"LabelTimeDurationXSeconds": "{0} secondi",
"LabelTimeInMinutes": "Tempo in minuti",
"LabelTimeLeft": "{0} sinistra",
"LabelTimeLeft": "{0} rimasti",
"LabelTimeListened": "Tempo di Ascolto",
"LabelTimeListenedToday": "Tempo di Ascolto Oggi",
"LabelTimeRemaining": "{0} rimanente",
@@ -682,7 +682,7 @@
"LabelTitle": "Titolo",
"LabelToolsEmbedMetadata": "Incorpora Metadata",
"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",
"LabelToolsMakeM4bDescription": "Genera un file audiolibro M4B con metadati incorporati, immagine di copertina e capitoli.",
"LabelToolsSplitM4b": "Converti M4B in MP3",
@@ -854,7 +854,7 @@
"MessageNoItems": "Nessun oggetto",
"MessageNoItemsFound": "Nessun oggetto trovato",
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
"MessageNoLogs": "Nessun Log",
"MessageNoLogs": "Nessun rapporto",
"MessageNoMediaProgress": "Nessun progresso multimediale",
"MessageNoNotifications": "Nessuna notifica",
"MessageNoPodcastFeed": "Podcast non valido: nessun feed",
@@ -1109,7 +1109,7 @@
"ToastProgressIsNotBeingSynced": "L'avanzamento non è sincronizzato, riavviare la riproduzione",
"ToastProviderCreatedFailed": "Impossibile aggiungere il provider",
"ToastProviderCreatedSuccess": "Aggiunto nuovo provider",
"ToastProviderNameAndUrlRequired": "Nome e URL richiesti",
"ToastProviderNameAndUrlRequired": "Nome e Url richiesti",
"ToastProviderRemoveSuccess": "Provider rimosso",
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
+3 -3
View File
@@ -392,7 +392,7 @@
"LabelGenre": "Жанр",
"LabelGenres": "Жанры",
"LabelHardDeleteFile": "Жесткое удаление файла",
"LabelHasEbook": "Есть e-книга",
"LabelHasEbook": "Есть электронная книга",
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
"LabelHideSubtitles": "Скрыть серии",
"LabelHighestPriority": "Наивысший приоритет",
@@ -437,8 +437,8 @@
"LabelLibraryItem": "Элемент библиотеки",
"LabelLibraryName": "Имя библиотеки",
"LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
"LabelLibrarySortByProgressFinished": "Прогресс: Завершено",
"LabelLibrarySortByProgressStarted": "Прогресс: Начато",
"LabelLibrarySortByProgressFinished": "Прогресс: Закончена",
"LabelLibrarySortByProgressStarted": "Прогресс: Начата",
"LabelLimit": "Лимит",
"LabelLineSpacing": "Межстрочный интервал",
"LabelListenAgain": "Послушать снова",
+1 -1
View File
@@ -275,7 +275,7 @@
"LabelBonus": "Bonus",
"LabelBooks": "Knihy",
"LabelButtonText": "Text tlačidla",
"LabelByAuthor": "od",
"LabelByAuthor": "od {0}",
"LabelChangePassword": "Zmeniť heslo",
"LabelChannels": "Kanály",
"LabelChapterCount": "{0} kapitol",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
"version": "2.33.1",
"version": "2.33.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "2.33.1",
"version": "2.33.2",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.33.1",
"version": "2.33.2",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
+3 -3
View File
@@ -10,7 +10,7 @@ const CacheManager = require('../managers/CacheManager')
const CoverManager = require('../managers/CoverManager')
const AuthorFinder = require('../finders/AuthorFinder')
const { reqSupportsWebp, isValidASIN } = require('../utils/index')
const { reqSupportsWebp, isValidASIN, clampPositiveInt } = require('../utils/index')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
@@ -412,8 +412,8 @@ class AuthorController {
const options = {
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
height: height ? parseInt(height) : null,
width: width ? parseInt(width) : null
height: clampPositiveInt(height ? parseInt(height) : null, 4096),
width: clampPositiveInt(width ? parseInt(width) : null, 4096)
}
return CacheManager.handleAuthorCache(res, authorId, options)
}
+1 -1
View File
@@ -117,7 +117,7 @@ class FileSystemController {
filepath = fileUtils.filePathToPOSIX(filepath)
// 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}`)
return res.sendStatus(400)
}
+9 -4
View File
@@ -462,7 +462,7 @@ class LibraryController {
}
}
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) {
@@ -563,7 +563,7 @@ class LibraryController {
mediaItemIds.push(libraryItem.mediaId)
}
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
@@ -714,7 +714,7 @@ class LibraryController {
}
}
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) {
@@ -1435,10 +1435,15 @@ class LibraryController {
const libraryItems = await Database.libraryItemModel.findAll({
attributes: ['id', 'libraryId', 'path', 'isFile'],
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}"`)
const filename = `LibraryItems-${Date.now()}.zip`
+5 -5
View File
@@ -7,7 +7,7 @@ const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database')
const zipHelpers = require('../utils/zipHelpers')
const { reqSupportsWebp } = require('../utils/index')
const { reqSupportsWebp, clampPositiveInt } = require('../utils/index')
const { ScanResult, AudioMimeType } = require('../utils/constants')
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
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) {
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
await fs.remove(libraryItemPath).catch((error) => {
@@ -398,8 +398,8 @@ class LibraryItemController {
const options = {
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
height: height ? parseInt(height) : null,
width: width ? parseInt(width) : null
height: clampPositiveInt(height ? parseInt(height) : null, 4096),
width: clampPositiveInt(width ? parseInt(width) : null, 4096)
}
return CacheManager.handleCoverCache(res, libraryItemId, options)
}
@@ -565,7 +565,7 @@ class LibraryItemController {
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) {
Logger.info(`[LibraryItemController] Deleting library item from file system at "${libraryItemPath}"`)
await fs.remove(libraryItemPath).catch((error) => {
+12 -2
View File
@@ -7,7 +7,7 @@ const Database = require('../Database')
const fs = require('../libs/fsExtra')
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 htmlSanitizer = require('../utils/htmlSanitizer')
@@ -58,8 +58,18 @@ class PodcastController {
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)
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
const existingLibraryItem =
(await Database.libraryItemModel.count({
@@ -83,7 +93,7 @@ class PodcastController {
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
let relPath = payload.path.replace(folder.fullPath, '')
let relPath = podcastPath.replace(libraryFolderPath, '')
if (relPath.startsWith('/')) relPath = relPath.slice(1)
let newLibraryItem = null
+38 -1
View File
@@ -126,13 +126,31 @@ class BackupManager {
} catch (error) {
// Not a valid zip file
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')
}
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.`)
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.')
}
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 details = data.toString('utf8').split('\n')
@@ -140,9 +158,13 @@ class BackupManager {
if (!backup.serverVersion) {
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.')
}
await zip.close().catch(() => {})
backup.fileSize = await getFileSize(backup.fullPath)
const existingBackupIndex = this.backups.findIndex((b) => b.id === backup.id)
@@ -257,9 +279,24 @@ class BackupManager {
let data = null
try {
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')
} catch (error) {
Logger.error(`[BackupManager] Failed to unzip backup "${fullFilePath}"`, error)
if (zip) await zip.close().catch(() => {})
continue
}
+4 -3
View File
@@ -111,16 +111,17 @@ class Author extends Model {
*
* @param {string} name
* @param {string} libraryId
* @returns {Promise<Author>}
* @returns {Promise<{ author: Author, created: boolean }>}
*/
static async findOrCreateByNameAndLibrary(name, libraryId) {
const author = await this.getByNameAndLibrary(name, libraryId)
if (author) return author
return this.create({
if (author) return { author, created: false }
const newAuthor = await this.create({
name,
lastFirst: this.getLastFirst(name),
libraryId
})
return { author: newAuthor, created: true }
}
/**
+12 -1
View File
@@ -4,6 +4,7 @@ const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
const parseNameString = require('../utils/parsers/parseNameString')
const htmlSanitizer = require('../utils/htmlSanitizer')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
const SocketAuthority = require('../SocketAuthority')
/**
* @typedef EBookFileObject
@@ -470,13 +471,23 @@ class Book extends Model {
for (const author of authorsRemoved) {
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}"`)
this.authors = this.authors.filter((au) => au.id !== author.id)
}
const authorsAdded = []
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 })
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}"`)
this.authors.push(author)
authorsAdded.push(author)
+2 -1
View File
@@ -110,7 +110,8 @@ class PlaybackSession {
startedAt: this.startedAt,
updatedAt: this.updatedAt,
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
}
}
+1 -1
View File
@@ -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]
}
get codecsToForceAAC() {
return ['alac', 'ac3', 'eac3']
return ['alac', 'ac3', 'eac3', 'opus']
}
get userToken() {
return this.user.token
+4 -2
View File
@@ -363,8 +363,9 @@ class ApiRouter {
* Remove library item and associated entities
* @param {string} libraryItemId
* @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({
where: {
mediaItemId: mediaItemIds
@@ -395,7 +396,8 @@ class ApiRouter {
await Database.libraryItemModel.removeById(libraryItemId)
SocketAuthority.emitter('item_removed', {
id: libraryItemId
id: libraryItemId,
libraryId
})
}
+2
View File
@@ -48,6 +48,8 @@ module.exports.AudioMimeType = {
AIF: 'audio/x-aiff',
WEBM: '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',
AWB: 'audio/amr-wb',
CAF: 'audio/x-caf',
+10
View File
@@ -54,6 +54,16 @@ module.exports.isNullOrNaN = (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) => {
return new Promise((resolve, reject) => {
parseString(xml, (err, results) => {
+4
View File
@@ -217,6 +217,10 @@ function extractEpisodeData(item) {
episode[cleanKey] = extractFirstArrayItemString(item, key)
})
if (episode.subtitle) {
episode.subtitle = htmlSanitizer.sanitize(episode.subtitle.trim())
}
// Extract psc:chapters if duration is set
episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null