diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst index e30bc490c..241a47010 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 e695ccb0c..f8543f1de 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 c7572ba50..3dcfb0495 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 18abc66e7..4bc434cbd 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 2d41d39ef..568274c80 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.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.31.0", + "version": "2.32.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 33d66c276..df71f6256 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.31.0", + "version": "2.32.0", "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 9d12d3096..881bd971b 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/pl.json b/client/strings/pl.json index 316e84234..32c9766ca 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -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/zh-cn.json b/client/strings/zh-cn.json index 1a95256ab..64c41619e 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 d703a8edc..4c9e9b9ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.31.0", + "version": "2.32.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.31.0", + "version": "2.32.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 0aaf4f5af..2b0d0b6cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.31.0", + "version": "2.32.0", "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 2c12ffc1a..6ba01aa83 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 = null + let tagsCleaned = null + + 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 c079a1280..5c8cad75a 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 206068cc4..af4405987 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) {