diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index fbed11be..15f34867 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -338,9 +338,15 @@ export default { libraryItemsAdded(libraryItems) { console.log('libraryItems added', libraryItems) - const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId) - if (!this.search && isThisLibrary) { - this.fetchCategories() + const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added') + if (!recentlyAddedShelf) return + + // Add new library item to the recently added shelf + for (const libraryItem of libraryItems) { + if (libraryItem.libraryId === this.currentLibraryId && !recentlyAddedShelf.entities.some((ent) => ent.id === libraryItem.id)) { + // Add to front of array + recentlyAddedShelf.entities.unshift(libraryItem) + } } }, libraryItemsUpdated(items) { diff --git a/client/components/modals/libraries/EditModal.vue b/client/components/modals/libraries/EditModal.vue index 5bcdabed..2a68dd63 100644 --- a/client/components/modals/libraries/EditModal.vue +++ b/client/components/modals/libraries/EditModal.vue @@ -127,7 +127,7 @@ export default { skipMatchingMediaWithIsbn: false, autoScanCronExpression: null, hideSingleBookSeries: false, - metadataPrecedence: ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } } }, diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index 215f79b5..8ec73dd0 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -19,9 +19,11 @@
  • reorder
    - {{ source.include ? index + 1 : '' }} + {{ source.include ? getSourceIndex(source.id) : '' }} +
    +
    + {{ source.name }} {{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}
    -
    {{ source.name }}
    @@ -64,6 +66,11 @@ export default { name: 'Audio file meta tags', include: true }, + nfoFile: { + id: 'nfoFile', + name: 'NFO file', + include: true + }, txtFiles: { id: 'txtFiles', name: 'desc.txt & reader.txt files', @@ -92,20 +99,34 @@ export default { }, isBookLibrary() { return this.mediaType === 'book' + }, + firstActiveSourceIndex() { + return this.metadataSourceMapped.findIndex((source) => source.include) + }, + lastActiveSourceIndex() { + return this.metadataSourceMapped.findLastIndex((source) => source.include) } }, methods: { + getSourceIndex(source) { + const activeSources = (this.librarySettings.metadataPrecedence || []).map((s) => s).reverse() + return activeSources.findIndex((s) => s === source) + 1 + }, resetToDefault() { this.metadataSourceMapped = [] for (const key in this.metadataSourceData) { this.metadataSourceMapped.push({ ...this.metadataSourceData[key] }) } + this.metadataSourceMapped.reverse() + this.$emit('update', this.getLibraryData()) }, getLibraryData() { + const metadataSourceIds = this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) + metadataSourceIds.reverse() return { settings: { - metadataPrecedence: this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s) + metadataPrecedence: metadataSourceIds } } }, @@ -120,15 +141,16 @@ export default { }, init() { const metadataPrecedence = this.librarySettings.metadataPrecedence || [] - this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s) for (const sourceKey in this.metadataSourceData) { if (!metadataPrecedence.includes(sourceKey)) { const unusedSourceData = { ...this.metadataSourceData[sourceKey], include: false } - this.metadataSourceMapped.push(unusedSourceData) + this.metadataSourceMapped.unshift(unusedSourceData) } } + + this.metadataSourceMapped.reverse() } }, mounted() { diff --git a/client/package-lock.json b/client/package-lock.json index 1dc72e4c..16adf9db 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.5.0", + "version": "2.6.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index c815d388..a13b5815 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.5.0", + "version": "2.6.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index e2f6d678..e645569e 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -11,6 +11,11 @@

    {{ $strings.HeaderOpenIDConnectAuthentication }}

    + + + help_outline + +
    diff --git a/client/strings/cs.json b/client/strings/cs.json index 26e7bcc9..ba027b92 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Trvale smazat soubor", "LabelHasEbook": "Obsahuje elektronickou knihu", "LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu", + "LabelHighestPriority": "Highest priority", "LabelHost": "Hostitel", "LabelHour": "Hodina", "LabelIcon": "Ikona", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Informace", "LabelLogLevelWarn": "Varovat", "LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Přehrávač médií", "LabelMediaType": "Typ média", - "LabelMetadataOrderOfPrecedenceDescription": "1 je nejnižší priorita, 5 je nejvyšší priorita", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Poskytovatel metadat", "LabelMetaTag": "Metaznačka", "LabelMetaTags": "Metaznačky", diff --git a/client/strings/da.json b/client/strings/da.json index d950be16..7ddc787e 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Permanent slet fil", "LabelHasEbook": "Har e-bog", "LabelHasSupplementaryEbook": "Har supplerende e-bog", + "LabelHighestPriority": "Highest priority", "LabelHost": "Vært", "LabelHour": "Time", "LabelIcon": "Ikon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Information", "LabelLogLevelWarn": "Advarsel", "LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Medieafspiller", "LabelMediaType": "Medietype", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadataudbyder", "LabelMetaTag": "Meta-tag", "LabelMetaTags": "Meta-tags", diff --git a/client/strings/de.json b/client/strings/de.json index cb399ca4..78e64804 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -92,7 +92,7 @@ "HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen", "HeaderAudiobookTools": "Hörbuch-Dateiverwaltungstools", "HeaderAudioTracks": "Audiodateien", - "HeaderAuthentication": "Authentication", + "HeaderAuthentication": "Authentifizierung", "HeaderBackups": "Sicherungen", "HeaderChangePassword": "Passwort ändern", "HeaderChapters": "Kapitel", @@ -132,10 +132,10 @@ "HeaderNewAccount": "Neues Konto", "HeaderNewLibrary": "Neue Bibliothek", "HeaderNotifications": "Benachrichtigungen", - "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", + "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung", "HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOtherFiles": "Sonstige Dateien", - "HeaderPasswordAuthentication": "Password Authentication", + "HeaderPasswordAuthentication": "Password Authentifizierung", "HeaderPermissions": "Berechtigungen", "HeaderPlayerQueue": "Spieler Warteschlange", "HeaderPlaylist": "Wiedergabeliste", @@ -184,11 +184,11 @@ "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", - "LabelAdminUsersOnly": "Admin users only", + "LabelAdminUsersOnly": "Nur Admin Benutzer", "LabelAll": "Alle", "LabelAllUsers": "Alle Benutzer", - "LabelAllUsersExcludingGuests": "All users excluding guests", - "LabelAllUsersIncludingGuests": "All users including guests", + "LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen", + "LabelAllUsersIncludingGuests": "All Benutzer und Gäste", "LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden", "LabelAppend": "Anhängen", "LabelAuthor": "Autor", @@ -196,10 +196,10 @@ "LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthors": "Autoren", "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", - "LabelAutoLaunch": "Auto Launch", - "LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path /login?autoLaunch=0)", - "LabelAutoRegister": "Auto Register", - "LabelAutoRegisterDescription": "Automatically create new users after logging in", + "LabelAutoLaunch": "Automatischer Start", + "LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad /login?autoLaunch=0)", + "LabelAutoRegister": "Automatische Registrierung", + "LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Einloggen", "LabelBackToUser": "Zurück zum Benutzer", "LabelBackupLocation": "Backup-Ort", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", @@ -240,7 +240,7 @@ "LabelDeselectAll": "Alles abwählen", "LabelDevice": "Gerät", "LabelDeviceInfo": "Geräteinformationen", - "LabelDeviceIsAvailableTo": "Device is available to...", + "LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...", "LabelDirectory": "Verzeichnis", "LabelDiscFromFilename": "CD aus dem Dateinamen", "LabelDiscFromMetadata": "CD aus den Metadaten", @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Datei dauerhaft löschen", "LabelHasEbook": "mit E-Book", "LabelHasSupplementaryEbook": "mit zusätlichem E-Book", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Stunde", "LabelIcon": "Symbol", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Informationen", "LabelLogLevelWarn": "Warnungen", "LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum", - "LabelMatchExistingUsersBy": "Match existing users by", - "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", + "LabelLowestPriority": "Lowest Priority", + "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", + "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet", "LabelMediaPlayer": "Mediaplayer", "LabelMediaType": "Medientyp", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadatenanbieter", "LabelMetaTag": "Meta Schlagwort", "LabelMetaTags": "Meta Tags", @@ -408,7 +410,7 @@ "LabelSeason": "Staffel", "LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", - "LabelSelectUsers": "Select users", + "LabelSelectUsers": "Benutzer auswählen", "LabelSendEbookToDevice": "E-Book senden an...", "LabelSequence": "Reihenfolge", "LabelSeries": "Serien", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index b7896281..8e1f6ce6 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/es.json b/client/strings/es.json index fde3782e..bbfbf267 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHasEbook": "Tiene Ebook", "LabelHasSupplementaryEbook": "Tiene Ebook Suplementario", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hora", "LabelIcon": "Icono", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Información", "LabelLogLevelWarn": "Advertencia", "LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Reproductor de Medios", "LabelMediaType": "Tipo de Multimedia", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Proveedor de Metadata", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/fr.json b/client/strings/fr.json index 97d2766e..8999222b 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Suppression du fichier", "LabelHasEbook": "Dispose d’un livre numérique", "LabelHasSupplementaryEbook": "Dispose d’un livre numérique supplémentaire", + "LabelHighestPriority": "Highest priority", "LabelHost": "Hôte", "LabelHour": "Heure", "LabelIcon": "Icone", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Lecteur multimédia", "LabelMediaType": "Type de média", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Fournisseur de métadonnées", "LabelMetaTag": "Etiquette de métadonnée", "LabelMetaTags": "Etiquettes de métadonnée", diff --git a/client/strings/gu.json b/client/strings/gu.json index 3fca0367..4318ad5a 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/hi.json b/client/strings/hi.json index 1f35e11a..69244330 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Hour", "LabelIcon": "Icon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/hr.json b/client/strings/hr.json index 5fe09ab2..2370dc33 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Obriši datoteku zauvijek", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Sat", "LabelIcon": "Ikona", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Media Type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Poslužitelj metapodataka ", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/it.json b/client/strings/it.json index 881cc19b..3f86bf2e 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Elimina Definitivamente", "LabelHasEbook": "Un ebook", "LabelHasSupplementaryEbook": "Un ebook Supplementare", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Ora", "LabelIcon": "Icona", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Allarme", "LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Media Player", "LabelMediaType": "Tipo Media", - "LabelMetadataOrderOfPrecedenceDescription": "1 e bassa priorità, 5 è alta priorità", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Provider", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/lt.json b/client/strings/lt.json index 00d3aeed..ae54e8bf 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Galutinai ištrinti failą", "LabelHasEbook": "Turi e-knygą", "LabelHasSupplementaryEbook": "Turi papildomą e-knygą", + "LabelHighestPriority": "Highest priority", "LabelHost": "Serveris", "LabelHour": "Valanda", "LabelIcon": "Piktograma", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Ieškoti naujų epizodų po šios datos", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Grotuvas", "LabelMediaType": "Medijos tipas", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metaduomenų tiekėjas", "LabelMetaTag": "Meta žymė", "LabelMetaTags": "Meta žymos", diff --git a/client/strings/nl.json b/client/strings/nl.json index 1421368b..d9399455 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Hard-delete bestand", "LabelHasEbook": "Heeft ebook", "LabelHasSupplementaryEbook": "Heeft supplementair ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Uur", "LabelIcon": "Icoon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Waarschuwing", "LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediaspeler", "LabelMediaType": "Mediatype", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadatabron", "LabelMetaTag": "Meta-tag", "LabelMetaTags": "Meta-tags", diff --git a/client/strings/no.json b/client/strings/no.json index 7d2acf3b..b4541229 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Tving sletting av fil", "LabelHasEbook": "Har ebok", "LabelHasSupplementaryEbook": "Har supplerende ebok", + "LabelHighestPriority": "Highest priority", "LabelHost": "Tjener", "LabelHour": "Time", "LabelIcon": "Ikon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Se etter nye episoder etter denne datoen", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediespiller", "LabelMediaType": "Medie type", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadata Leverandør", "LabelMetaTag": "Meta Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/pl.json b/client/strings/pl.json index b0521f0a..51587fed 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Usuń trwale plik", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHighestPriority": "Highest priority", "LabelHost": "Host", "LabelHour": "Godzina", "LabelIcon": "Ikona", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Informacja", "LabelLogLevelWarn": "Ostrzeżenie", "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Odtwarzacz", "LabelMediaType": "Typ mediów", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Dostawca metadanych", "LabelMetaTag": "Tag", "LabelMetaTags": "Meta Tags", diff --git a/client/strings/ru.json b/client/strings/ru.json index 851d2ba3..c0fd0cf7 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Жесткое удаление файла", "LabelHasEbook": "Есть e-книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга", + "LabelHighestPriority": "Highest priority", "LabelHost": "Хост", "LabelHour": "Часы", "LabelIcon": "Иконка", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Медиа проигрыватель", "LabelMediaType": "Тип медиа", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Провайдер", "LabelMetaTag": "Мета тег", "LabelMetaTags": "Мета теги", diff --git a/client/strings/sv.json b/client/strings/sv.json index eea30043..6bb0eec2 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "Hård radering av fil", "LabelHasEbook": "Har e-bok", "LabelHasSupplementaryEbook": "Har kompletterande e-bok", + "LabelHighestPriority": "Highest priority", "LabelHost": "Värd", "LabelHour": "Timme", "LabelIcon": "Ikon", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "Felsökningsnivå: Information", "LabelLogLevelWarn": "Felsökningsnivå: Varning", "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "Mediaspelare", "LabelMediaType": "Mediatyp", - "LabelMetadataOrderOfPrecedenceDescription": "1 är lägsta prioritet, 5 är högsta prioritet", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "Metadataleverantör", "LabelMetaTag": "Metamärke", "LabelMetaTags": "Metamärken", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index 8bb242a4..2f20a42e 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -283,6 +283,7 @@ "LabelHardDeleteFile": "完全删除文件", "LabelHasEbook": "有电子书", "LabelHasSupplementaryEbook": "有补充电子书", + "LabelHighestPriority": "Highest priority", "LabelHost": "主机", "LabelHour": "小时", "LabelIcon": "图标", @@ -324,11 +325,12 @@ "LabelLogLevelInfo": "信息", "LabelLogLevelWarn": "警告", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", + "LabelLowestPriority": "Lowest Priority", "LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMediaPlayer": "媒体播放器", "LabelMediaType": "媒体类型", - "LabelMetadataOrderOfPrecedenceDescription": "1 is lowest priority, 5 is highest priority", + "LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources", "LabelMetadataProvider": "元数据提供者", "LabelMetaTag": "元数据标签", "LabelMetaTags": "元标签", diff --git a/package-lock.json b/package-lock.json index e1a5f266..9df54fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.5.0", + "version": "2.6.0", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", @@ -9487,4 +9487,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 477f62af..061e2a7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.5.0", + "version": "2.6.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", @@ -61,4 +61,4 @@ "nyc": "^15.1.0", "sinon": "^17.0.1" } -} +} \ No newline at end of file diff --git a/server/Auth.js b/server/Auth.js index dedf32f0..57792177 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -363,12 +363,50 @@ class Auth { req.session[sessionKey].code_verifier = req.query.code_verifier } + function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { + Logger.error(logMessage) + if (response) { + // Depending on the error, it can also have a body + // We also log the request header the passport plugin sents for the URL + const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED') + Logger.debug(header + '\n' + response.body?.toString()) + } + + if (isMobile) { + return res.status(errorCode).send(errorMessage) + } else { + return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`) + } + } + + function passportCallback(req, res, next) { + return (err, user, info) => { + const isMobile = req.session[sessionKey]?.mobile === true + if (err) { + return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response) + } + + if (!user) { + // Info usually contains the error message from the SSO provider + return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response) + } + + req.logIn(user, (loginError) => { + if (loginError) { + return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`) + } + next() + }) + } + } + + // While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request // We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided if (req.session[sessionKey].mobile) { - return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next) + return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next) } else { - return passport.authenticate('openid-client', { failureRedirect: '/login?error=Unauthorized&autoLaunch=0' })(req, res, next) + return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next) } }, // on a successfull login: read the cookies and react like the client requested (callback or json) diff --git a/server/Logger.js b/server/Logger.js index 5eb33a24..b4953189 100644 --- a/server/Logger.js +++ b/server/Logger.js @@ -5,6 +5,7 @@ class Logger { constructor() { this.isDev = process.env.NODE_ENV !== 'production' this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE + this.hideDevLogs = process.env.HIDE_DEV_LOGS === undefined ? !this.isDev : process.env.HIDE_DEV_LOGS === '1' this.socketListeners = [] this.logManager = null @@ -92,7 +93,7 @@ class Logger { * @param {...any} args */ dev(...args) { - if (!this.isDev || process.env.HIDE_DEV_LOGS === '1') return + if (this.hideDevLogs) return console.log(`[${this.timestamp}] DEV:`, ...args) } diff --git a/server/Server.js b/server/Server.js index 4883fb71..5e8cab76 100644 --- a/server/Server.js +++ b/server/Server.js @@ -231,6 +231,7 @@ class Server { '/library/:library/search', '/library/:library/bookshelf/:id?', '/library/:library/authors', + '/library/:library/narrators', '/library/:library/series/:id?', '/library/:library/podcast/search', '/library/:library/podcast/latest', diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js index b734b6bf..10ee19e0 100644 --- a/server/objects/settings/LibrarySettings.js +++ b/server/objects/settings/LibrarySettings.js @@ -9,7 +9,7 @@ class LibrarySettings { this.autoScanCronExpression = null this.audiobooksOnly = false this.hideSingleBookSeries = false // Do not show series that only have 1 book - this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] if (settings) { this.construct(settings) @@ -28,7 +28,7 @@ class LibrarySettings { this.metadataPrecedence = [...settings.metadataPrecedence] } else { // Added in v2.4.5 - this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] } } diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 282155f2..48e8529a 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -18,6 +18,7 @@ const BookFinder = require('../finders/BookFinder') const LibraryScan = require("./LibraryScan") const OpfFileScanner = require('./OpfFileScanner') +const NfoFileScanner = require('./NfoFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') /** @@ -593,7 +594,7 @@ class BookScanner { } const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId) - const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata'] + const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] libraryScan.addLog(LogLevel.DEBUG, `"${bookMetadata.title}" Getting metadata with precedence [${metadataPrecedence.join(', ')}]`) for (const metadataSource of metadataPrecedence) { if (bookMetadataSourceHandler[metadataSource]) { @@ -649,6 +650,14 @@ class BookScanner { AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan) } + /** + * Metadata from .nfo file + */ + async nfoFile() { + if (!this.libraryItemData.metadataNfoLibraryFile) return + await NfoFileScanner.scanBookNfoFile(this.libraryItemData.metadataNfoLibraryFile, this.bookMetadata) + } + /** * Description from desc.txt and narrator from reader.txt */ diff --git a/server/scanner/LibraryItemScanData.js b/server/scanner/LibraryItemScanData.js index 576280c8..b604e4d7 100644 --- a/server/scanner/LibraryItemScanData.js +++ b/server/scanner/LibraryItemScanData.js @@ -132,6 +132,11 @@ class LibraryItemScanData { return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf') } + /** @type {LibraryItem.LibraryFileObject} */ + get metadataNfoLibraryFile() { + return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo') + } + /** * * @param {LibraryItem} existingLibraryItem diff --git a/server/scanner/NfoFileScanner.js b/server/scanner/NfoFileScanner.js new file mode 100644 index 00000000..e450b5c3 --- /dev/null +++ b/server/scanner/NfoFileScanner.js @@ -0,0 +1,48 @@ +const { parseNfoMetadata } = require('../utils/parsers/parseNfoMetadata') +const { readTextFile } = require('../utils/fileUtils') + +class NfoFileScanner { + constructor() { } + + /** + * Parse metadata from .nfo file found in library scan and update bookMetadata + * + * @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj + * @param {Object} bookMetadata + */ + async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) { + const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path) + const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null + if (nfoMetadata) { + for (const key in nfoMetadata) { + if (key === 'tags') { // Add tags only if tags are empty + if (nfoMetadata.tags.length) { + bookMetadata.tags = nfoMetadata.tags + } + } else if (key === 'genres') { // Add genres only if genres are empty + if (nfoMetadata.genres.length) { + bookMetadata.genres = nfoMetadata.genres + } + } else if (key === 'authors') { + if (nfoMetadata.authors?.length) { + bookMetadata.authors = nfoMetadata.authors + } + } else if (key === 'narrators') { + if (nfoMetadata.narrators?.length) { + bookMetadata.narrators = nfoMetadata.narrators + } + } else if (key === 'series') { + if (nfoMetadata.series) { + bookMetadata.series = [{ + name: nfoMetadata.series, + sequence: nfoMetadata.sequence || null + }] + } + } else if (nfoMetadata[key] && key !== 'sequence') { + bookMetadata[key] = nfoMetadata[key] + } + } + } + } +} +module.exports = new NfoFileScanner() \ No newline at end of file diff --git a/server/utils/parsers/parseNfoMetadata.js b/server/utils/parsers/parseNfoMetadata.js new file mode 100644 index 00000000..56e9400a --- /dev/null +++ b/server/utils/parsers/parseNfoMetadata.js @@ -0,0 +1,100 @@ +function parseNfoMetadata(nfoText) { + if (!nfoText) return null + const lines = nfoText.split(/\r?\n/) + const metadata = {} + let insideBookDescription = false + lines.forEach(line => { + if (line.search(/^\s*book description\s*$/i) !== -1) { + insideBookDescription = true + return + } + if (insideBookDescription) { + if (line.search(/^\s*=+\s*$/i) !== -1) return + metadata.description = metadata.description || '' + metadata.description += line + '\n' + return + } + const match = line.match(/^(.*?):(.*)$/) + if (match) { + const key = match[1].toLowerCase().trim() + const value = match[2].trim() + if (!value) return + switch (key) { + case 'title': + { + const titleMatch = value.match(/^(.*?):(.*)$/) + if (titleMatch) { + metadata.title = titleMatch[1].trim() + metadata.subtitle = titleMatch[2].trim() + } else { + metadata.title = value + } + } + break + case 'author': + metadata.authors = value.split(/\s*,\s*/).filter(v => v) + break + case 'narrator': + case 'read by': + metadata.narrators = value.split(/\s*,\s*/).filter(v => v) + break + case 'series name': + metadata.series = value + break + case 'genre': + metadata.genres = value.split(/\s*,\s*/).filter(v => v) + break + case 'tags': + metadata.tags = value.split(/\s*,\s*/).filter(v => v) + break + case 'copyright': + case 'audible.com release': + case 'audiobook copyright': + case 'book copyright': + case 'recording copyright': + case 'release date': + case 'date': + { + const year = extractYear(value) + if (year) { + metadata.publishedYear = year + } + } + break + case 'position in series': + metadata.sequence = value + break + case 'unabridged': + metadata.abridged = value.toLowerCase() === 'yes' ? false : true + break + case 'abridged': + metadata.abridged = value.toLowerCase() === 'no' ? false : true + break + case 'publisher': + metadata.publisher = value + break + case 'asin': + metadata.asin = value + break + case 'isbn': + case 'isbn-10': + case 'isbn-13': + metadata.isbn = value + break + } + } + }) + + // Trim leading/trailing whitespace for description + if (metadata.description) { + metadata.description = metadata.description.trim() + } + + return metadata +} +module.exports = { parseNfoMetadata } + +function extractYear(str) { + const match = str.match(/\d{4}/g) + return match ? match[match.length - 1] : null +} \ No newline at end of file diff --git a/test/server/utils/parsers/parseNfoMetadata.test.js b/test/server/utils/parsers/parseNfoMetadata.test.js new file mode 100644 index 00000000..70e6a096 --- /dev/null +++ b/test/server/utils/parsers/parseNfoMetadata.test.js @@ -0,0 +1,123 @@ +const chai = require('chai') +const expect = chai.expect +const { parseNfoMetadata } = require('../../../../server/utils/parsers/parseNfoMetadata') + +describe('parseNfoMetadata', () => { + it('returns null if nfoText is empty', () => { + const result = parseNfoMetadata('') + expect(result).to.be.null + }) + + it('parses title', () => { + const nfoText = 'Title: The Great Gatsby' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('The Great Gatsby') + }) + + it('parses title with subtitle', () => { + const nfoText = 'Title: The Great Gatsby: A Novel' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.equal('The Great Gatsby') + expect(result.subtitle).to.equal('A Novel') + }) + + it('parses authors', () => { + const nfoText = 'Author: F. Scott Fitzgerald' + const result = parseNfoMetadata(nfoText) + expect(result.authors).to.deep.equal(['F. Scott Fitzgerald']) + }) + + it('parses multiple authors', () => { + const nfoText = 'Author: John Steinbeck, Ernest Hemingway' + const result = parseNfoMetadata(nfoText) + expect(result.authors).to.deep.equal(['John Steinbeck', 'Ernest Hemingway']) + }) + + it('parses narrators', () => { + const nfoText = 'Read by: Jake Gyllenhaal' + const result = parseNfoMetadata(nfoText) + expect(result.narrators).to.deep.equal(['Jake Gyllenhaal']) + }) + + it('parses multiple narrators', () => { + const nfoText = 'Read by: Jake Gyllenhaal, Kate Winslet' + const result = parseNfoMetadata(nfoText) + expect(result.narrators).to.deep.equal(['Jake Gyllenhaal', 'Kate Winslet']) + }) + + it('parses series name', () => { + const nfoText = 'Series Name: Harry Potter' + const result = parseNfoMetadata(nfoText) + expect(result.series).to.equal('Harry Potter') + }) + + it('parses genre', () => { + const nfoText = 'Genre: Fiction' + const result = parseNfoMetadata(nfoText) + expect(result.genres).to.deep.equal(['Fiction']) + }) + + it('parses multiple genres', () => { + const nfoText = 'Genre: Fiction, Historical' + const result = parseNfoMetadata(nfoText) + expect(result.genres).to.deep.equal(['Fiction', 'Historical']) + }) + + it('parses tags', () => { + const nfoText = 'Tags: mystery, thriller' + const result = parseNfoMetadata(nfoText) + expect(result.tags).to.deep.equal(['mystery', 'thriller']) + }) + + it('parses year from various date fields', () => { + const nfoText = 'Release Date: 2021-05-01\nBook Copyright: 2021\nRecording Copyright: 2021' + const result = parseNfoMetadata(nfoText) + expect(result.publishedYear).to.equal('2021') + }) + + it('parses position in series', () => { + const nfoText = 'Position in Series: 2' + const result = parseNfoMetadata(nfoText) + expect(result.sequence).to.equal('2') + }) + + it('parses abridged flag', () => { + const nfoText = 'Abridged: No' + const result = parseNfoMetadata(nfoText) + expect(result.abridged).to.be.false + + const nfoText2 = 'Unabridged: Yes' + const result2 = parseNfoMetadata(nfoText2) + expect(result2.abridged).to.be.false + }) + + it('parses publisher', () => { + const nfoText = 'Publisher: Penguin Random House' + const result = parseNfoMetadata(nfoText) + expect(result.publisher).to.equal('Penguin Random House') + }) + + it('parses ASIN', () => { + const nfoText = 'ASIN: B08X5JZJLH' + const result = parseNfoMetadata(nfoText) + expect(result.asin).to.equal('B08X5JZJLH') + }) + + it('parses description', () => { + const nfoText = 'Book Description\n=========\nThis is a book.\n It\'s good' + const result = parseNfoMetadata(nfoText) + expect(result.description).to.equal('This is a book.\n It\'s good') + }) + + it('no value', () => { + const nfoText = 'Title:' + const result = parseNfoMetadata(nfoText) + expect(result.title).to.be.undefined + }) + + it('no year value', () => { + const nfoText = "Date:0" + const result = parseNfoMetadata(nfoText) + expect(result.publishedYear).to.be.undefined + }) +}) \ No newline at end of file