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] }) }