diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst
index e30bc490..241a4701 100644
--- a/build/debian/DEBIAN/preinst
+++ b/build/debian/DEBIAN/preinst
@@ -22,7 +22,7 @@ add_user() {
declare -r descr="${4:-No description}"
declare -r shell="${5:-/bin/false}"
- if ! getent passwd | grep -q "^$user:"; then
+ if ! getent passwd "$user" 2>&1 >/dev/null; then
echo "Creating system user: $user in $group with $descr and shell $shell"
useradd $uid_flags --gid $group --no-create-home --system --shell $shell -c "$descr" $user
fi
@@ -39,7 +39,7 @@ add_group() {
declare -r gid_flags="--gid $gid"
fi
- if ! getent group | grep -q "^$group:" ; then
+ if ! getent group "$group" 2>&1 >/dev/null; then
echo "Creating system group: $group"
groupadd $gid_flags --system $group
fi
diff --git a/client/components/modals/playlists/AddCreateModal.vue b/client/components/modals/playlists/AddCreateModal.vue
index e695ccb0..f8543f1d 100644
--- a/client/components/modals/playlists/AddCreateModal.vue
+++ b/client/components/modals/playlists/AddCreateModal.vue
@@ -97,7 +97,10 @@ export default {
...playlist
}
})
- .sort((a, b) => (a.isItemIncluded ? -1 : 1))
+ .sort((a, b) => {
+ if (a.isItemIncluded !== b.isItemIncluded) return a.isItemIncluded ? -1 : 1
+ return a.name.localeCompare(b.name)
+ })
},
isBatch() {
return this.selectedPlaylistItems.length > 1
diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue
index c7572ba5..3dcfb049 100644
--- a/client/components/ui/MultiSelect.vue
+++ b/client/components/ui/MultiSelect.vue
@@ -278,7 +278,7 @@ export default {
})
},
insertNewItem(item) {
- this.selected.push(item)
+ if (!this.selected.includes(item)) this.selected.push(item)
this.$emit('input', this.selected)
this.$emit('newItem', item)
this.textInput = null
diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue
index 18abc66e..4bc434cb 100644
--- a/client/components/ui/MultiSelectQueryInput.vue
+++ b/client/components/ui/MultiSelectQueryInput.vue
@@ -287,7 +287,7 @@ export default {
})
},
insertNewItem(item) {
- this.selected.push(item)
+ if (!this.selected.find((i) => i.name === item.name)) this.selected.push(item)
this.$emit('input', this.selected)
this.$emit('newItem', item)
this.textInput = null
diff --git a/client/package-lock.json b/client/package-lock.json
index 2d41d39e..1e2d52c1 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
- "version": "2.31.0",
+ "version": "2.32.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
- "version": "2.31.0",
+ "version": "2.32.1",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
diff --git a/client/package.json b/client/package.json
index 33d66c27..0eaffb10 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
- "version": "2.31.0",
+ "version": "2.32.1",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
diff --git a/client/strings/el.json b/client/strings/el.json
index 9d12d309..881bd971 100644
--- a/client/strings/el.json
+++ b/client/strings/el.json
@@ -49,6 +49,7 @@
"ButtonMapChapterTitles": "Χαρτογράφηση Τίτλων Κεφαλαίων",
"ButtonMatchAllAuthors": "Αντιστοίχιση Όλων των Συγγραφέων",
"ButtonMatchBooks": "Αντιστοίχιση Βιβλίων",
+ "ButtonNevermind": "Άστο",
"ButtonNext": "Επόμενο",
"ButtonNextChapter": "Επόμενο Κεφάλαιο",
"ButtonNextItemInQueue": "Επόμενο Αντικείμενο στην Ουρά",
@@ -62,8 +63,13 @@
"ButtonPlaylists": "Λίστες Αναπαραγωγής",
"ButtonPrevious": "Προηγούμενο",
"ButtonPreviousChapter": "Προηγούμενο Κεφάλαιο",
+ "ButtonProbeAudioFile": "Ανάλυση Αρχείου Ήχου",
+ "ButtonPurgeAllCache": "Εκκαθάριση Όλης της Προσωρινής Μνήμης",
+ "ButtonPurgeItemsCache": "Εκκαθάριση της Μνήμης Αντικειμένων",
"ButtonQueueAddItem": "Προσθήκη στην ουρά",
"ButtonQueueRemoveItem": "Αφαίρεση απ'την ουρά",
+ "ButtonQuickEmbed": "Γρήγορη Ενσωμάτωση",
+ "ButtonQuickEmbedMetadata": "Γρήγορη Ενσωμάτωση Μεταδεδομένων",
"ButtonQuickMatch": "Γρήγορη Αντιστοίχηση",
"ButtonReScan": "Επανασάρωση",
"ButtonRead": "Ανάγνωση",
@@ -73,35 +79,237 @@
"ButtonRemove": "Αφαίρεση",
"ButtonRemoveAll": "Αφαίρεση Όλων",
"ButtonRemoveAllLibraryItems": "Αφαίρεση Όλων των Αντικειμέων Βιβλιοθήκης",
+ "ButtonRemoveFromContinueListening": "Αφαίρεση από τη Συνέχεια Ακρόασης",
+ "ButtonRemoveFromContinueReading": "Αφαίρεση από τη Συνέχεια Ανάγνωσης",
+ "ButtonRemoveSeriesFromContinueSeries": "Αφαίρεση Σειράς από τη Συνέχεια Σειράς",
"ButtonReset": "Επαναφορά",
"ButtonResetToDefault": "Επαναφορά στις προεπιλογές",
"ButtonRestore": "Επαναφορά",
"ButtonSave": "Αποθήκευση",
"ButtonSaveAndClose": "Αποθήκευση και Κλείσιμο",
+ "ButtonSaveTracklist": "Αποθήκευση Λίστας Κομματιών",
"ButtonScan": "Σάρψση",
+ "ButtonScanLibrary": "Σάρωση Βιβλιοθήκης",
+ "ButtonScrollLeft": "Κύλιση Αριστερά",
+ "ButtonScrollRight": "Κύλιση Δεξιά",
"ButtonSearch": "Αναζήτηση",
+ "ButtonSelectFolderPath": "Επιλογή Διαδρομής Φακέλου",
"ButtonSeries": "Σειρά",
+ "ButtonSetChaptersFromTracks": "Ορισμός κεφαλαίων από τα κομμάτια",
+ "ButtonShare": "Κοινοποίηση",
+ "ButtonShiftTimes": "Χρόνοι Μετακίνησης",
+ "ButtonShow": "Εμφάνιση",
+ "ButtonStartM4BEncode": "Έναρξη Κωδικοποίησης M4B",
+ "ButtonStats": "Στατιστικά",
"ButtonSubmit": "Υποβολή",
+ "ButtonTest": "Δοκιμή",
+ "ButtonUpload": "Μεταφόρτωση",
+ "ButtonUploadBackup": "Μεταφόρτωση Αντιγράφου Ασφαλείας",
+ "ButtonUploadCover": "Μεταφόρτωση Εξωφύλλου",
+ "ButtonUploadOPMLFile": "Μεταφόρτωση Αρχείου OPML",
+ "ButtonUserDelete": "Διαγραφή Χρήστη {0}",
+ "ButtonUserEdit": "Επεξεργασίας χρήστη {0}",
+ "ButtonViewAll": "Εμφάνιση Όλων",
"ButtonYes": "Ναι",
+ "ErrorUploadLacksTitle": "Πρέπει να έχει τίτλο",
"HeaderAccount": "Λογαριασμός",
"HeaderAdvanced": "Για Προχωρημένους",
+ "HeaderApiKeys": "Κλειδιά API",
"HeaderAudioTracks": "Κομμάτια Ήχου",
+ "HeaderBackups": "Αντίγραφα Ασφαλείας",
+ "HeaderBulkChapterModal": "Προσθήκη Πολλαπλών Κεφαλαίων",
+ "HeaderChangePassword": "Αλλαγή Κωδικού Πρόσβασης",
"HeaderChapters": "Κεφάλαια",
+ "HeaderChooseAFolder": "Επιλογή Φακέλου",
"HeaderCollection": "Συλλογή",
"HeaderCollectionItems": "Αντικείμενα Συλλογής",
+ "HeaderCover": "Εξώφυλλο",
+ "HeaderCurrentDownloads": "Τρέχουσες Λήψεις",
"HeaderDetails": "Λεπτομέρειες",
+ "HeaderDownloadQueue": "Ουρά Λήψης",
"HeaderEbookFiles": "Αρχεία Ebook",
+ "HeaderEmail": "Ηλεκτρονικό Ταχυδρομίο",
+ "HeaderEmailSettings": "Ρυθμίσεις Ηλεκτρονικού Ταχυδρομίου",
"HeaderEpisodes": "Επεισόδια",
"HeaderEreaderSettings": "Ρυθμίσεις Ereader",
+ "HeaderFiles": "Αρχεία",
+ "HeaderFindChapters": "Εύρεση Κεφαλαίων",
+ "HeaderItemFiles": "Αρχεία Αντικειμένων",
+ "HeaderLastListeningSession": "Τελευταία Συνεδρία Ακρόασης",
"HeaderLatestEpisodes": "Τελευταία Επεισόδια",
"HeaderLibraries": "Βιβλιοθήκες",
+ "HeaderLibraryFiles": "Αρχεία Βιβλιοθήκης",
+ "HeaderLibraryStats": "Στατιστικά Βιβλιοθήκης",
+ "HeaderListeningSessions": "Συνεδρίες Ακρόασης",
+ "HeaderListeningStats": "Στατιστικά Ακρόασης",
+ "HeaderMatch": "Ταύτιση",
+ "HeaderNewAccount": "Νέος Λογαριασμός",
+ "HeaderNewApiKey": "Νέο Κλειδί API",
+ "HeaderNewLibrary": "Νέα Βιβλιοθήκη",
+ "HeaderNotificationCreate": "Δημιουργία Ειδοποίησης",
+ "HeaderNotificationUpdate": "Ενημέρωση Ειδοποίησης",
+ "HeaderNotifications": "Ειδοποιήσεις",
"HeaderOpenRSSFeed": "Άνοιγμα Τροφοδοσίας RSS",
+ "HeaderOtherFiles": "Άλλα Αρχεία",
+ "HeaderPermissions": "Δικαιώματα",
+ "HeaderPlayerSettings": "Ρυθμίσεις Αναπαραγωγής",
"HeaderPlaylist": "Λίστα Αναπαραγωγής",
"HeaderPlaylistItems": "Αντικείμενα Λίστας Αναπαραγωγής",
+ "HeaderPresets": "Προεπιλογές",
"HeaderRSSFeedGeneral": "Λεπτομέρειες RSS",
"HeaderRSSFeedIsOpen": "Η Τροφοδοσία RSS είναι Ανοιχτή",
+ "HeaderRemoveEpisode": "Αφαίρεση Επεισοδίου",
+ "HeaderSession": "Συνεδρία",
+ "HeaderSetBackupSchedule": "Ορισμός Προγράμματος Αντιγράφων Ασφαλείας",
"HeaderSettings": "Ρυθμίσεις",
+ "HeaderSettingsDisplay": "Προβολή",
+ "HeaderSettingsGeneral": "Γενικά",
+ "HeaderSettingsSecurity": "Ασφάλεια",
+ "HeaderSleepTimer": "Χρονοδιακόπτης Ύπνου",
+ "HeaderStatsLargestItems": "Μεγαλύτερα Αντικείμενα",
+ "HeaderStatsLongestItems": "Μεγαλύτερα Αντικείμενα (ώρες)",
"HeaderStatsMinutesListeningChart": "Λεπτά Ακρόασης (τελευταίες 7 ημέρες)",
"HeaderStatsRecentSessions": "Πρόσφατες Συνεδρίες",
- "HeaderTableOfContents": "Πίνακας Περιεχομένων"
+ "HeaderStatsTop10Authors": "10 Κορυφαίου Συγγραφείς",
+ "HeaderStatsTop5Genres": "5 Κορυφαία Είδη",
+ "HeaderTableOfContents": "Πίνακας Περιεχομένων",
+ "HeaderTools": "Εργαλεία",
+ "HeaderUpdateAccount": "Ενημέρωση Λογαριασμού",
+ "HeaderUpdateApiKey": "Ενημέρωση Κλειδιού API",
+ "HeaderUpdateAuthor": "Ενημέρωση Συγγραφέα",
+ "HeaderUpdateDetails": "Ενημέρωση Λεπτομερειεών",
+ "HeaderUpdateLibrary": "Ενημέρωση Βιβλιοθήκης",
+ "HeaderUsers": "Χρήστες",
+ "HeaderYourStats": "Τα Στατιστικά Σας",
+ "LabelAbridged": "Συνοπτικό",
+ "LabelAccessibleBy": "Προσβάσιμο από",
+ "LabelAccountType": "Τύπος Λογαριασμού",
+ "LabelAccountTypeAdmin": "Διαχειριστής",
+ "LabelAccountTypeGuest": "Επισκέπτης",
+ "LabelAccountTypeUser": "Χρήστης",
+ "LabelAddToCollection": "Προσθήκη σε Συλλογή",
+ "LabelAddToCollectionBatch": "Προσθήκη {0} Βιβλίων στην Συλλογή",
+ "LabelAddToPlaylist": "Προσθήκη στην Λίστα Αναπαραγωγής",
+ "LabelAddedAt": "Προστέθηκε Στις",
+ "LabelAddedDate": "Προστέθηκε {0}",
+ "LabelAll": "Όλα",
+ "LabelAllEpisodesDownloaded": "Όλα τα επεισόδια λήφθηκαν",
+ "LabelAllUsers": "Όλοι οι Χρήστες",
+ "LabelAlreadyInYourLibrary": "Υπάρχει ήδη στην βιβλιοθήκη",
+ "LabelAudioChannels": "Κανάλια Ήχου (1 ή 2)",
+ "LabelAuthor": "Συγγραφέας",
+ "LabelAuthorFirstLast": "Συγγραφέας (Όνομα Επώνυμο)",
+ "LabelAuthorLastFirst": "Συγγραφέας (Επώνυμο, Όνομα)",
+ "LabelAuthors": "Συγγραφείς",
+ "LabelAutoDownloadEpisodes": "Αυτόματο Κατέβασμα Επεισοδίων",
+ "LabelAutoLaunch": "Αυτόματη Εκκίνηση",
+ "LabelBackupLocation": "Τοποθεσία Αντιγράφου Ασφαλείας",
+ "LabelBackupsEnableAutomaticBackups": "Αυτόματα αντίγραφα ασφαλείας",
+ "LabelBackupsNumberToKeep": "Αριθμός αντιγράφων ασφαλείας προς διατήρηση",
+ "LabelBooks": "Βιβλία",
+ "LabelButtonText": "Κείμενο Κουμπιού",
+ "LabelByAuthor": "κατά {0}",
+ "LabelChangePassword": "Αλλαγή Κωδικού Πρόσβασης",
+ "LabelChannels": "Κανάλια",
+ "LabelChapterCount": "{0} Κεφάλαια",
+ "LabelChapterTitle": "Τίτλος Κεφαλαίου",
+ "LabelChapters": "Κεφάλαια",
+ "LabelChaptersFound": "κεφάλαια βρέθηκαν",
+ "LabelClosePlayer": "Κλείσιμο αναπαραγωγής",
+ "LabelCollection": "Συλλογή",
+ "LabelCollections": "Συλλογές",
+ "LabelComplete": "Ολοκλήρωση",
+ "LabelConfirmPassword": "Επιβεβαίωση Κωδικού Πρόσβασης",
+ "LabelContinueListening": "Συνέχεια Ακρόασης",
+ "LabelContinueReading": "Συνέχεια Ανάγνωσης",
+ "LabelContinueSeries": "Συνέχεια Σειράς",
+ "LabelCover": "Εξώφυλλο",
+ "LabelCoverImageURL": "URL Εικόνας Εξωφύλλου",
+ "LabelCoverProvider": "Πάροχος Εξωφύλλου",
+ "LabelCreatedAt": "Δημιουρήθηκε Στις",
+ "LabelCurrent": "Τρέχων",
+ "LabelCurrently": "Τρέχων:",
+ "LabelDays": "Ημέρες",
+ "LabelDescription": "Περιγραφή",
+ "LabelDevice": "Συσκευή",
+ "LabelDeviceInfo": "Πληροφορίες Συσκευής",
+ "LabelDownload": "Λήψη",
+ "LabelDownloadNEpisodes": "Λήψη {0} επεισοδίων",
+ "LabelDuration": "Διάρκεια",
+ "LabelDurationComparisonExactMatch": "(ακριβής ταύτιση)",
+ "LabelEbook": "Ebook",
+ "LabelEbooks": "Ebooks",
+ "LabelEdit": "Επεξεργασία",
+ "LabelEmail": "Ηλεκτρονικό Ταχυδρομίο",
+ "LabelEmailSettingsFromAddress": "Από Διεύθυνση",
+ "LabelEmailSettingsSecure": "Ασφαλές",
+ "LabelEmailSettingsTestAddress": "Δοκιμή Διεύθυνσης",
+ "LabelEmbeddedCover": "Ενσωματωμένο Εξώφυλλο",
+ "LabelEnable": "Ενεργοποίηση",
+ "LabelEnd": "Τέλος",
+ "LabelEndOfChapter": "Τέλος Κεφαλαίου",
+ "LabelEpisode": "Επεισόδιο",
+ "LabelFile": "Αρχείο",
+ "LabelFilename": "Όνομα Αρχείου",
+ "LabelFinished": "Ολοκληρώθηκε",
+ "LabelFolder": "Φάκελος",
+ "LabelFontFamily": "Οικογένεια Γραμματοσειράς",
+ "LabelGenre": "Είδος",
+ "LabelGenres": "Είδη",
+ "LabelHost": "Διακομιστής",
+ "LabelInProgress": "Σε Εξέλιξη",
+ "LabelLanguage": "Γλώσσα",
+ "LabelLayoutSinglePage": "Μονή Σελίδα",
+ "LabelListenAgain": "Επανάληψη Ακρόασης",
+ "LabelMediaType": "Τύπος Πολυμέσων",
+ "LabelMore": "Περισσότερα",
+ "LabelMoreInfo": "Περισσότερες Πληροφορίες",
+ "LabelName": "Όνομα",
+ "LabelNarrator": "Αφηγητής",
+ "LabelNarrators": "Αφηγητές",
+ "LabelNewestAuthors": "Πρόσφατοι Συγγραφείς",
+ "LabelNewestEpisodes": "Πρόσφατα Επεισόδια",
+ "LabelNotStarted": "Δεν Έχει Ξεκινήσει",
+ "LabelNumberOfEpisodes": "# Επεισοδίων",
+ "LabelPassword": "Κωδικός Πρόσβασης",
+ "LabelPath": "Διαδρομή",
+ "LabelProgress": "Πρόοδος",
+ "LabelPublishYear": "Χρονολογία Έκδοσης",
+ "LabelPublishedDate": "Εκδόθηκε {0}",
+ "LabelRandomly": "Τυχαία",
+ "LabelRead": "Ανάγνωση",
+ "LabelReadAgain": "Ανάγνωση Ξανά",
+ "LabelRecentSeries": "Πρόσφατη Σειρά",
+ "LabelRecentlyAdded": "Προστέθηκαν Πρόσφατα",
+ "LabelSeries": "Σειρά",
+ "LabelSetEbookAsPrimary": "Ορισμός ως πρωτεύων",
+ "LabelShowAll": "Εμφάνιση Όλων",
+ "LabelSize": "Μέγεθος",
+ "LabelSleepTimer": "Χρονοδιακόπτης Ύπνου",
+ "LabelStart": "Έναρξη",
+ "LabelStatsBestDay": "Καλύτερη Ημέρα",
+ "LabelStatsDailyAverage": "Ημερήσιος Μέσος Όρος",
+ "LabelStatsDays": "Ημέρες",
+ "LabelStatsDaysListened": "Ημέρες Ακρόασης",
+ "LabelStatsInARow": "Σε σειρά",
+ "LabelStatsItemsFinished": "Ολοκληρωμένα Αντικείμενα",
+ "LabelStatsMinutes": "λεπτά",
+ "LabelStatsMinutesListening": "Λεπτά Ακρόασης",
+ "LabelStatsWeekListening": "Εβδομαδιαία Ακρόαση",
+ "LabelTheme": "Θέμα",
+ "LabelThemeDark": "Σκοτεινό",
+ "LabelThemeLight": "Φωτεινό",
+ "LabelTimeRemaining": "{0} απομένουν",
+ "LabelTitle": "Τίτλος",
+ "LabelTracks": "Κομμάτια",
+ "LabelType": "Τύπος",
+ "LabelUnknown": "Άγνωστο",
+ "LabelUser": "Χρήστης",
+ "LabelUsername": "Όνομα Χρήστη",
+ "LabelYourProgress": "Η Πρόοδος Σας",
+ "MessageDownloadingEpisode": "Λήψη επεισοδίου",
+ "MessageLoading": "Φόρτωση...",
+ "MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
+ "MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
+ "MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
}
diff --git a/client/strings/fi.json b/client/strings/fi.json
index 600db265..dde7009e 100644
--- a/client/strings/fi.json
+++ b/client/strings/fi.json
@@ -275,7 +275,7 @@
"LabelBonus": "Bonus",
"LabelBooks": "Kirjat",
"LabelButtonText": "Painikkeen teksti",
- "LabelByAuthor": "tekijältä {0}",
+ "LabelByAuthor": "Tekijältä: {0}",
"LabelChangePassword": "Vaihda salasana",
"LabelChannels": "Kanavat",
"LabelChapterCount": "{0} lukua",
@@ -790,6 +790,7 @@
"MessageConfirmRemoveAuthor": "Oletko varma, että haluat poistaa tekijän \"{0}\"?",
"MessageConfirmRemoveCollection": "Oletko varma, että haluat poistaa kokoelman \"{0}\"?",
"MessageConfirmRemoveEpisode": "Oletko varma, että haluat poistaa jakson \"{0}\"?",
+ "MessageConfirmRemoveEpisodeNote": "Huomioi: Tämä ei poista äänitiedostoa, ellei \"Poista tiedosto pysyvästi\" -asetusta ole valittuna",
"MessageConfirmRemoveEpisodes": "Oletko varma, että haluat poistaa {0} jaksoa?",
"MessageConfirmRemoveListeningSessions": "Oletko varma, että haluat poistaa {0} kuuntelukertaa?",
"MessageConfirmRemoveMetadataFiles": "Oletko varma, että haluat poistaa kaikki metadata.{0}-tiedostot kirjaston kohdekansioista?",
@@ -816,6 +817,7 @@
"MessageFetching": "Haetaan...",
"MessageForceReScanDescription": "skannaa kaikki tiedostot uudelleen kuten uusi tarkistus. Äänitiedoston ID3-tunnisteet, OPF-tiedostot ja tekstitiedostot skannataan uusina.",
"MessageHeatmapListeningTimeTooltip": "{0} kuunnellaan on {1}",
+ "MessageHeatmapNoListeningSessions": "Ei kuuntelujaksoja {0}",
"MessageImportantNotice": "Tärkeä huomautus!",
"MessageInsertChapterBelow": "Syötä luku alle",
"MessageInvalidAsin": "Virheellinen ASIN",
@@ -886,10 +888,11 @@
"MessageResetChaptersConfirm": "Oletko varma, että haluat nollata luvut ja kumota tekemäsi muutokset?",
"MessageRestoreBackupConfirm": "Oletko varma, että haluat palauttaa varmuuskopion, joka on luotu",
"MessageRestoreBackupWarning": "Varmuuskopion palauttaminen korvaa koko /config:ssa sijaitsevan tietokannan, ja kansikuvat /metadata/items & /metadata/authors:ssa.
Varmuuskopiot eivät muuta kirjastokansioissasi olevia tiedostoja. Jos olet ottanut käyttöön palvelinasetuksissa kansikuvien ja metatietojen tallentamisen kirjaston kansioihin, niitä ei varmuuskopioida tai korvata.
Kaikki palvelintasi käyttävät asiakkaat virkistetään automaattisesti.",
- "MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja \"Tarkkaile kirjaston muutoksia automaattisesti\" -asetus pidetään käytössä - se havaitsee muutokset kirjastokansioissasi automaattisesti. Ota tämä ominaisuus käyttöön, jos \"Tarkkaile kirjaston muutoksia automaattisesti\" ei toimi tiedostojärjestelmässäsi (kuten NFS).\"",
+ "MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja \"Tarkkaile kirjaston muutoksia automaattisesti\" -asetus pidetään käytössä - se havaitsee muutokset kirjastokansioissasi automaattisesti. Ota tämä ominaisuus käyttöön, jos \"Tarkkaile kirjaston muutoksia automaattisesti\" ei toimi tiedostojärjestelmässäsi (kuten NFS).",
"MessageScheduleRunEveryWeekdayAtTime": "Suorita joka {0} klo {1}",
"MessageSearchResultsFor": "Hakutulokset haulle",
"MessageSelected": "{0} valittuna",
+ "MessageSeriesSequenceCannotContainSpaces": "Sarjan sekvenssi ei voi sisältää välilyöntejä",
"MessageServerCouldNotBeReached": "Palvelimelle ei saatu yhteyttä",
"MessageSetChaptersFromTracksDescription": "Aseta luvut käyttämällä kutakin äänitiedostoa lukuna ja luvun otsikkoa äänitiedoston nimenä",
"MessageShareExpirationWillBe": "Umpeutuminen on {0}",
@@ -951,7 +954,10 @@
"NotificationOnBackupCompletedDescription": "Laukaistu, kun varmuuskopiointi on valmis",
"NotificationOnBackupFailedDescription": "Laukaistu, kun varmuuskopiointi epäonnistuu",
"NotificationOnEpisodeDownloadedDescription": "Laukaistu, kun podcast-jakso ladataan automaattisesti",
+ "NotificationOnRSSFeedDisabledDescription": "Laukaistaan, kun automaattiset jaksolataukset poistetaan käytöstä liian monen epäonnistuneen yrityksen vuoksi",
+ "NotificationOnRSSFeedFailedDescription": "Laukaistaan, kun RRS-syötteen pyyntö epäonnistuu automaattisessa jaksolatauksessa",
"NotificationOnTestDescription": "Tapahtuma ilmoitusjärjestelmän testaamista varten",
+ "PlaceholderBulkChapterInput": "Syötä luvun otsikko tai käytä numerointia (esim. 'Episodi 1', 'Luku 10', '1.')",
"PlaceholderNewCollection": "Uusi kokoelman nimi",
"PlaceholderNewFolderPath": "Uusi kansion polku",
"PlaceholderNewPlaylist": "Uusi soittolistan nimi",
@@ -1005,15 +1011,23 @@
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
"ToastBookmarkCreateSuccess": "Kirjanmerkki lisätty",
"ToastBookmarkRemoveSuccess": "Kirjanmerkki poistettu",
+ "ToastBulkChapterInvalidCount": "Syötä numero 1 ja 150 välillä",
"ToastCachePurgeFailed": "Välimuistin tyhjentäminen epäonnistui",
"ToastCachePurgeSuccess": "Välimuisti tyhjennetty onnistuneesti",
+ "ToastChapterLocked": "Luku on lukittu.",
+ "ToastChapterStartTimeAdjusted": "Luvun aloitusaikaa on säädetty {0} sekunnilla",
+ "ToastChaptersAllLocked": "Kaikki luvut ovat lukittuina. Avaa lukuja vaihtaaksesi niiden aikoja.",
"ToastChaptersHaveErrors": "Luvuissa on virheitä",
+ "ToastChaptersInvalidShiftAmountLast": "Virheellinen siirtomäärä. Viimeisen luvun aloitusaika ylittäisi tämän äänikirjan keston.",
+ "ToastChaptersInvalidShiftAmountStart": "Virheellinen siirtomäärä. Ensimmäisen luvun pituudeksi tulisi nolla tai negatiivinen arvo, ja toinen luku kirjoittaisi sen päälle. Kasvata toisen luvun aloitusaikaa.",
"ToastChaptersMustHaveTitles": "Lukuilla on oltava otsikot",
"ToastChaptersRemoved": "Luvut poistettu",
"ToastChaptersUpdated": "Luvut päivitetty",
"ToastCollectionItemsAddFailed": "Kohteen/kohteiden lisääminen kokoelmaan epäonnistui",
"ToastCollectionRemoveSuccess": "Kokoelma poistettu",
"ToastCollectionUpdateSuccess": "Kokoelma päivitetty",
+ "ToastConnectionNotAvailable": "Verkkoyhteyttä ei saatavilla. Yritä hetken päästä uudelleen",
+ "ToastCoverSearchFailed": "Kansikuvan haku epäonnistui",
"ToastCoverUpdateFailed": "Kansikuvan päivitys epäonnistui",
"ToastDateTimeInvalidOrIncomplete": "Päivämäärä ja aika ovat epäkelvolliset tai puutteelliset",
"ToastDeleteFileFailed": "Tiedoston poistaminen epäonnistui",
@@ -1029,6 +1043,8 @@
"ToastEpisodeDownloadQueueClearSuccess": "Jakson latausjono tyhjennetty",
"ToastEpisodeUpdateSuccess": "{0} jaksoa päivitetty",
"ToastErrorCannotShare": "Ei voi jakaa alkuperäisesti tällä laitteella",
+ "ToastFailedToCreate": "Luonti epäonnistui",
+ "ToastFailedToDelete": "Poisto epäonnistui",
"ToastFailedToLoadData": "Tietojen lataaminen epäonnistui",
"ToastFailedToMatch": "Vastaaminen epäonnistui",
"ToastFailedToShare": "Jakaminen epäonnistui",
@@ -1036,6 +1052,7 @@
"ToastInvalidImageUrl": "Epäkelvollinen kuvan URL-osoite",
"ToastInvalidMaxEpisodesToDownload": "Ladattavien jaksojen enimmäismäärä on epäkelvollinen",
"ToastInvalidUrl": "Epäkelvollinen URL-osoite",
+ "ToastInvalidUrls": "Yksi tai useampi URL on virheellinen",
"ToastItemCoverUpdateSuccess": "Kohteen kansikuva päivitetty",
"ToastItemDeletedFailed": "Kohteen poistaminen epäonnistui",
"ToastItemDeletedSuccess": "Poistettu kohde",
@@ -1060,6 +1077,7 @@
"ToastMustHaveAtLeastOnePath": "On oltava vähintään yksi polku",
"ToastNameEmailRequired": "Nimi ja sähköpostiosoite vaaditaan",
"ToastNameRequired": "Nimi vaaditaan",
+ "ToastNewApiKeyUserError": "Täytyy valita käyttäjä",
"ToastNewEpisodesFound": "{0} uutta jaksoa löydetty",
"ToastNewUserCreatedFailed": "Tilin \"{0}\" luominen epäonnistui",
"ToastNewUserCreatedSuccess": "Uusi tili luotu",
@@ -1084,6 +1102,7 @@
"ToastPlaylistUpdateSuccess": "Soittolista päivitetty",
"ToastPodcastCreateFailed": "Podcastin luominen epäonnistui",
"ToastPodcastCreateSuccess": "Podcastin luominen onnistui",
+ "ToastPodcastEpisodeUpdated": "Episodi päivitetty",
"ToastPodcastGetFeedFailed": "Podcast-syötteen saaminen epäonnistui",
"ToastPodcastNoEpisodesInFeed": "RSS-syötteestä ei löytynyt jaksoja",
"ToastPodcastNoRssFeed": "Podcastilla ei ole RSS-syötettä",
@@ -1134,5 +1153,13 @@
"ToastUserPasswordChangeSuccess": "Salasana vaihdettu onnistuneesti",
"ToastUserPasswordMismatch": "Salasanat eivät täsmää",
"ToastUserPasswordMustChange": "Uusi salasana ei voi olla sama kuin vanha salasana",
- "ToastUserRootRequireName": "Pääkäyttäjän nimi on pakollinen"
+ "ToastUserRootRequireName": "Pääkäyttäjän nimi on pakollinen",
+ "TooltipAddChapters": "Lisää luku tai lukuja",
+ "TooltipAddOneSecond": "Lisää 1 sekunti",
+ "TooltipAdjustChapterStart": "Napauta säätääksesi aloitusaikaa",
+ "TooltipLockAllChapters": "Lukitse kaikki luvut",
+ "TooltipLockChapter": "Lukitse luku (Shift+napauta valitaksesi alueen)",
+ "TooltipSubtractOneSecond": "Vähennä 1 sekunti",
+ "TooltipUnlockAllChapters": "Avaa kaikki luvut",
+ "TooltipUnlockChapter": "Avaa luku (Shift+napauta valitaksesi alueen)"
}
diff --git a/client/strings/pl.json b/client/strings/pl.json
index 316e8423..8b70a134 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -96,7 +96,7 @@
"ButtonScrollRight": "Przewiń w prawo",
"ButtonSearch": "Szukaj",
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
- "ButtonSeries": "Serial",
+ "ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Ustawiaj rozdziały na podstawie utworów",
"ButtonShare": "Udostępnij",
"ButtonShiftTimes": "Przesunięcie czasowe",
@@ -233,8 +233,8 @@
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
"LabelAddToPlaylist": "Dodaj do playlisty",
"LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty",
- "LabelAddedAt": "Dodano w",
- "LabelAddedDate": "Dodano",
+ "LabelAddedAt": "Dodano",
+ "LabelAddedDate": "Dodano {0}",
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
"LabelAll": "Wszystkie",
"LabelAllEpisodesDownloaded": "Wszystkie odcinki pobrane",
diff --git a/client/strings/ru.json b/client/strings/ru.json
index e0b04864..c84fe9dc 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -275,7 +275,7 @@
"LabelBonus": "Бонус",
"LabelBooks": "Книги",
"LabelButtonText": "Текст кнопки",
- "LabelByAuthor": "{0}",
+ "LabelByAuthor": "от {0}",
"LabelChangePassword": "Изменить пароль",
"LabelChannels": "Ленты",
"LabelChapterCount": "{0} Главы",
diff --git a/client/strings/sv.json b/client/strings/sv.json
index 7bd9753b..d2fb254e 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -821,7 +821,7 @@
"MessageImportantNotice": "Viktig meddelande!",
"MessageInsertChapterBelow": "Infoga kapitel nedanför",
"MessageInvalidAsin": "Felaktig ASIN-kod",
- "MessageItemsSelected": "{0} objekt markerade",
+ "MessageItemsSelected": "{0} objekt valda",
"MessageItemsUpdated": "{0} objekt uppdaterade",
"MessageJoinUsOn": "Anslut dig till oss på",
"MessageLoading": "Laddar...",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 1a95256a..64c41619 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -275,7 +275,7 @@
"LabelBonus": "额外",
"LabelBooks": "图书",
"LabelButtonText": "按钮文本",
- "LabelByAuthor": "由 {0}",
+ "LabelByAuthor": "作者: {0}",
"LabelChangePassword": "修改密码",
"LabelChannels": "声道",
"LabelChapterCount": "{0} 章节",
diff --git a/package-lock.json b/package-lock.json
index 648b94e6..8f891665 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
- "version": "2.31.0",
+ "version": "2.32.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
- "version": "2.31.0",
+ "version": "2.32.1",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
diff --git a/package.json b/package.json
index 6d1c4e92..36de265b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "2.31.0",
+ "version": "2.32.1",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
diff --git a/server/providers/Audible.js b/server/providers/Audible.js
index 2c12ffc1..133d3c0d 100644
--- a/server/providers/Audible.js
+++ b/server/providers/Audible.js
@@ -57,8 +57,13 @@ class Audible {
})
}
- const genresFiltered = genres ? genres.filter((g) => g.type == 'genre').map((g) => g.name) : []
- const tagsFiltered = genres ? genres.filter((g) => g.type == 'tag').map((g) => g.name) : []
+ let genresCleaned = []
+ let tagsCleaned = []
+
+ if (genres && Array.isArray(genres)) {
+ genresCleaned = [...new Set(genres.filter((g) => g.type == 'genre').map((g) => g.name))]
+ tagsCleaned = [...new Set(genres.filter((g) => g.type == 'tag').map((g) => g.name))]
+ }
return {
title,
@@ -71,8 +76,8 @@ class Audible {
cover: image,
asin,
isbn,
- genres: genresFiltered.length ? genresFiltered : null,
- tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
+ genres: genresCleaned.length ? genresCleaned : null,
+ tags: tagsCleaned.length ? tagsCleaned : null,
series: series.length ? series : null,
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0,
diff --git a/server/providers/CustomProviderAdapter.js b/server/providers/CustomProviderAdapter.js
index c079a128..5c8cad75 100644
--- a/server/providers/CustomProviderAdapter.js
+++ b/server/providers/CustomProviderAdapter.js
@@ -89,6 +89,27 @@ class CustomProviderAdapter {
})
.filter((s) => s !== undefined)
}
+ /**
+ * Validates and dedupes tags/genres array
+ * Can be comma separated string or array of strings
+ * @param {string|string[]} tagsGenres
+ * @returns {string[]}
+ */
+ const validateTagsGenresArray = (tagsGenres) => {
+ if (!tagsGenres || (typeof tagsGenres !== 'string' && !Array.isArray(tagsGenres))) return undefined
+
+ // If string, split by comma and trim each item
+ if (typeof tagsGenres === 'string') tagsGenres = tagsGenres.split(',')
+ // If array, ensure all items are strings
+ else if (!tagsGenres.every((t) => typeof t === 'string')) return undefined
+
+ // Trim and filter out empty strings
+ tagsGenres = tagsGenres.map((t) => t.trim()).filter(Boolean)
+ if (!tagsGenres.length) return undefined
+
+ // Dedup
+ return [...new Set(tagsGenres)]
+ }
// re-map keys to throw out
return matches.map((match) => {
@@ -105,8 +126,8 @@ class CustomProviderAdapter {
cover: toStringOrUndefined(cover),
isbn: toStringOrUndefined(isbn),
asin: toStringOrUndefined(asin),
- genres: Array.isArray(genres) && genres.every((g) => typeof g === 'string') ? genres : undefined,
- tags: toStringOrUndefined(tags),
+ genres: validateTagsGenresArray(genres),
+ tags: validateTagsGenresArray(tags),
series: validateSeriesArray(series),
language: toStringOrUndefined(language),
duration: !isNaN(duration) && duration !== null ? Number(duration) : undefined
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index 206068cc..af440598 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -259,18 +259,17 @@ class Scanner {
SocketAuthority.emitter('author_added', author.toOldJSON())
// Update filter data
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
-
- await Database.bookAuthorModel
- .create({
- authorId: author.id,
- bookId: libraryItem.media.id
- })
- .then(() => {
- Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Added author "${author.name}" to "${libraryItem.media.title}"`)
- libraryItem.media.authors.push(author)
- hasAuthorUpdates = true
- })
}
+ await Database.bookAuthorModel
+ .create({
+ authorId: author.id,
+ bookId: libraryItem.media.id
+ })
+ .then(() => {
+ Logger.info(`[Scanner] quickMatchBookBuildUpdatePayload: Added author "${author.name}" to "${libraryItem.media.title}"`)
+ libraryItem.media.authors.push(author)
+ hasAuthorUpdates = true
+ })
}
const authorsRemoved = libraryItem.media.authors.filter((a) => !matchData.author.find((ma) => ma.toLowerCase() === a.name.toLowerCase()))
if (authorsRemoved.length) {
diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js
index 494a9564..7ae1dc86 100644
--- a/server/utils/queries/libraryItemsBookFilters.js
+++ b/server/utils/queries/libraryItemsBookFilters.js
@@ -236,7 +236,7 @@ module.exports = {
} else if (group === 'publishedDecades') {
const startYear = parseInt(value)
const endYear = parseInt(value, 10) + 9
- mediaWhere = Sequelize.where(Sequelize.literal('CAST(`book`.`publishedYear` AS INTEGER)'), {
+ mediaWhere = Sequelize.where(Sequelize.literal('CAST(publishedYear AS INTEGER)'), {
[Sequelize.Op.between]: [startYear, endYear]
})
}