diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 3c0caac9..b01542f1 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -921,7 +921,7 @@ export default { return null }) if (!libraryItem) return - this.store.commit('showEReader', libraryItem) + this.store.commit('showEReader', { libraryItem, keepProgress: true }) }, selectBtnClick(evt) { if (this.processingBatch) return diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index 3b52588c..bf2423bf 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -1,6 +1,6 @@ @@ -51,7 +54,8 @@ export default { menuWidth: { type: Number, default: 192 - } + }, + processing: Boolean }, data() { return { diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index ecdfe3f7..78c43b97 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -147,7 +147,9 @@ - + + + @@ -320,6 +322,9 @@ export default { libraryFiles() { return this.libraryItem.libraryFiles || [] }, + ebookFiles() { + return this.libraryFiles.filter((lf) => lf.fileType === 'ebook') + }, ebookFile() { return this.media.ebookFile }, @@ -519,7 +524,7 @@ export default { this.$store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'cover' }) }, openEbook() { - this.$store.commit('showEReader', this.libraryItem) + this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: true }) }, toggleFinished(confirmed = false) { if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) { diff --git a/client/store/index.js b/client/store/index.js index 89ff486c..1d49b8e8 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -17,6 +17,8 @@ export const state = () => ({ editPodcastModalTab: 'details', showEditModal: false, showEReader: false, + ereaderKeepProgress: false, + ereaderFileId: null, selectedLibraryItem: null, developerMode: false, processingBatch: false, @@ -210,8 +212,10 @@ export const mutations = { setEditPodcastModalTab(state, tab) { state.editPodcastModalTab = tab }, - showEReader(state, libraryItem) { + showEReader(state, { libraryItem, keepProgress, fileId }) { state.selectedLibraryItem = libraryItem + state.ereaderKeepProgress = keepProgress + state.ereaderFileId = fileId state.showEReader = true }, diff --git a/client/store/libraries.js b/client/store/libraries.js index 5901347b..a34a93c5 100644 --- a/client/store/libraries.js +++ b/client/store/libraries.js @@ -57,6 +57,9 @@ export const getters = { if (!getters.getCurrentLibrarySettings || isNaN(getters.getCurrentLibrarySettings.coverAspectRatio)) return 1 return getters.getCurrentLibrarySettings.coverAspectRatio === Constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1 }, + getLibraryIsAudiobooksOnly: (state, getters) => { + return !!getters.getCurrentLibrarySettings?.audiobooksOnly + }, getCollection: state => id => { return state.collections.find(c => c.id === id) }, diff --git a/client/strings/de.json b/client/strings/de.json index 1e9e9df1..f9993e4e 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Aktuelle Downloads", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Warteschlange", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episoden", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)", "LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Fortschritt", "LabelProvider": "Anbieter", "LabelPubDate": "Veröffentlichungsdatum", "LabelPublisher": "Herausgeber", "LabelPublishYear": "Jahr", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Kürzlich hinzugefügt", "LabelRecentSeries": "Aktuelle Serien", "LabelRecommended": "Empfohlen", @@ -366,6 +370,10 @@ "LabelSeries": "Serien", "LabelSeriesName": "Serienname", "LabelSeriesProgress": "Serienfortschritt", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden", "LabelSettingsChromecastSupport": "Chromecastunterstützung", "LabelSettingsDateFormat": "Datumsformat", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)", "MessageCheckingCron": "Überprüfe Cron...", "MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?", "MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?", "MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?", diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 73f12dc0..c1a08d7a 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Current Downloads", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episodes", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Progress", "LabelProvider": "Provider", "LabelPubDate": "Pub Date", "LabelPublisher": "Publisher", "LabelPublishYear": "Publish Year", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", "LabelRecommended": "Recommended", @@ -366,6 +370,10 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves", "LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsDateFormat": "Date Format", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", "MessageCheckingCron": "Checking cron...", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", diff --git a/client/strings/es.json b/client/strings/es.json index 696e1098..8a7598ca 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Descargando Actualmente", "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Lista de Descarga", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episodios", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)", "LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Progreso", "LabelProvider": "Proveedor", "LabelPubDate": "Fecha de Publicación", "LabelPublisher": "Editor", "LabelPublishYear": "Año de Publicación", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Agregado Reciente", "LabelRecentSeries": "Series Recientes", "LabelRecommended": "Recomendados", @@ -366,6 +370,10 @@ "LabelSeries": "Series", "LabelSeriesName": "Nombre de la Serie", "LabelSeriesProgress": "Progreso de la Serie", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Diseño Skeumorphic con Estantes de Madera", "LabelSettingsChromecastSupport": "Soporte para Chromecast", "LabelSettingsDateFormat": "Formato de Fecha", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "El comienzo del capítulo es después del final de su audiolibro", "MessageCheckingCron": "Checking cron...", "MessageConfirmDeleteBackup": "Esta seguro que desea eliminar el respaldo {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?", "MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?", "MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?", diff --git a/client/strings/fr.json b/client/strings/fr.json index 600c3768..8b9516ae 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Téléchargements en cours", "HeaderDetails": "Détails", "HeaderDownloadQueue": "File d'attente de téléchargements", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "E-mails", "HeaderEmailSettings": "Configuration des e-mails", "HeaderEpisodes": "Épisodes", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)", "LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de données iTunes et Google podcast", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Progression", "LabelProvider": "Fournisseur", "LabelPubDate": "Date de publication", "LabelPublisher": "Éditeur", "LabelPublishYear": "Année d’édition", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Derniers ajouts", "LabelRecentSeries": "Séries récentes", "LabelRecommended": "Recommandé", @@ -366,6 +370,10 @@ "LabelSeries": "Séries", "LabelSeriesName": "Nom de la série", "LabelSeriesProgress": "Progression de séries", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois", "LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsDateFormat": "Format de date", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Le Chapitre Début est situé au début de votre Livre Audio", "MessageCheckingCron": "Vérification du cron…", "MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?", "MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?", "MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?", diff --git a/client/strings/gu.json b/client/strings/gu.json index d565bcb0..47df0bfe 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Current Downloads", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episodes", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Progress", "LabelProvider": "Provider", "LabelPubDate": "Pub Date", "LabelPublisher": "Publisher", "LabelPublishYear": "Publish Year", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", "LabelRecommended": "Recommended", @@ -366,6 +370,10 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves", "LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsDateFormat": "Date Format", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", "MessageCheckingCron": "Checking cron...", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", diff --git a/client/strings/hi.json b/client/strings/hi.json index 123b8755..bbecb2cc 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Current Downloads", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episodes", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)", "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Progress", "LabelProvider": "Provider", "LabelPubDate": "Pub Date", "LabelPublisher": "Publisher", "LabelPublishYear": "Publish Year", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Recently Added", "LabelRecentSeries": "Recent Series", "LabelRecommended": "Recommended", @@ -366,6 +370,10 @@ "LabelSeries": "Series", "LabelSeriesName": "Series Name", "LabelSeriesProgress": "Series Progress", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves", "LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsDateFormat": "Date Format", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Chapter start is after the end of your audiobook", "MessageCheckingCron": "Checking cron...", "MessageConfirmDeleteBackup": "Are you sure you want to delete backup for {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", diff --git a/client/strings/hr.json b/client/strings/hr.json index 23ab4fc8..cee130fa 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Current Downloads", "HeaderDetails": "Detalji", "HeaderDownloadQueue": "Download Queue", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Epizode", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)", "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Napredak", "LabelProvider": "Dobavljač", "LabelPubDate": "Datam izdavanja", "LabelPublisher": "Izdavač", "LabelPublishYear": "Godina izdavanja", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Nedavno dodano", "LabelRecentSeries": "Nedavne serije", "LabelRecommended": "Recommended", @@ -366,6 +370,10 @@ "LabelSeries": "Serije", "LabelSeriesName": "Ime serije", "LabelSeriesProgress": "Series Progress", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Skeumorfski (što god to bilo) dizajn sa drvenim policama", "LabelSettingsChromecastSupport": "Chromecast podrška", "LabelSettingsDateFormat": "Format datuma", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Početak poglavlja je nakon kraja audioknjige.", "MessageCheckingCron": "Provjeravam cron...", "MessageConfirmDeleteBackup": "Jeste li sigurni da želite obrisati backup za {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?", "MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?", "MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?", diff --git a/client/strings/it.json b/client/strings/it.json index bee9d951..b72dcc4f 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Current Downloads", "HeaderDetails": "Dettagli", "HeaderDownloadQueue": "Download Queue", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Episodi", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)", "LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Cominciati", "LabelProvider": "Provider", "LabelPubDate": "Data Pubblicazione", "LabelPublisher": "Editore", "LabelPublishYear": "Anno Pubblicazione", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Aggiunti Recentemente", "LabelRecentSeries": "Serie Recenti", "LabelRecommended": "Raccomandati", @@ -366,6 +370,10 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nome Serie", "LabelSeriesProgress": "Cominciato", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Design con scaffali in legno", "LabelSettingsChromecastSupport": "Supporto a Chromecast", "LabelSettingsDateFormat": "Formato Data", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "L'inizio del capitolo è dopo la fine del tuo audiolibro", "MessageCheckingCron": "Controllo cron...", "MessageConfirmDeleteBackup": "Sei sicuro di voler eliminare il backup {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?", "MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?", "MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?", diff --git a/client/strings/nl.json b/client/strings/nl.json index 59935d77..a3398593 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Huidige downloads", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download-wachtrij", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Afleveringen", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)", "LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Voortgang", "LabelProvider": "Bron", "LabelPubDate": "Publicatiedatum", "LabelPublisher": "Uitgever", "LabelPublishYear": "Jaar van uitgave", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Recent toegevoegd", "LabelRecentSeries": "Recente series", "LabelRecommended": "Aangeraden", @@ -366,6 +370,10 @@ "LabelSeries": "Serie", "LabelSeriesName": "Naam serie", "LabelSeriesProgress": "Voortgang serie", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken", "LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsDateFormat": "Datum format", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek", "MessageCheckingCron": "Cron aan het checken...", "MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?", "MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?", "MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?", diff --git a/client/strings/pl.json b/client/strings/pl.json index 6c463272..e431f077 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Current Downloads", "HeaderDetails": "Szczegóły", "HeaderDownloadQueue": "Download Queue", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Rozdziały", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)", "LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Postęp", "LabelProvider": "Dostawca", "LabelPubDate": "Data publikacji", "LabelPublisher": "Wydawca", "LabelPublishYear": "Rok publikacji", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Niedawno dodany", "LabelRecentSeries": "Ostatnie serie", "LabelRecommended": "Recommended", @@ -366,6 +370,10 @@ "LabelSeries": "Serie", "LabelSeriesName": "Nazwy serii", "LabelSeriesProgress": "Postęp w serii", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Widok półki z ksiązkami", "LabelSettingsChromecastSupport": "Wsparcie Chromecast", "LabelSettingsDateFormat": "Format daty", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka", "MessageCheckingCron": "Sprawdzanie cron...", "MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?", "MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?", "MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?", diff --git a/client/strings/ru.json b/client/strings/ru.json index 3c7d5613..a68411fa 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "Текущие закачки", "HeaderDetails": "Подробности", "HeaderDownloadQueue": "Очередь скачивания", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "Эпизоды", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)", "LabelPreventIndexing": "Запретить индексацию фида каталогами подкастов iTunes и Google", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "Прогресс", "LabelProvider": "Провайдер", "LabelPubDate": "Дата публикации", "LabelPublisher": "Издатель", "LabelPublishYear": "Год публикации", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "Недавно добавленные", "LabelRecentSeries": "Последние серии", "LabelRecommended": "Рекомендованное", @@ -366,6 +370,10 @@ "LabelSeries": "Серия", "LabelSeriesName": "Имя серии", "LabelSeriesProgress": "Прогресс серии", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками", "LabelSettingsChromecastSupport": "Поддержка Chromecast", "LabelSettingsDateFormat": "Формат даты", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "Глава начинается после окончания аудиокниги", "MessageCheckingCron": "Проверка cron...", "MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?", "MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?", "MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index bfa66fae..1875104b 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -98,6 +98,7 @@ "HeaderCurrentDownloads": "当前下载", "HeaderDetails": "详情", "HeaderDownloadQueue": "下载队列", + "HeaderEbookFiles": "Ebook Files", "HeaderEmail": "Email", "HeaderEmailSettings": "Email Settings", "HeaderEpisodes": "剧集", @@ -339,12 +340,15 @@ "LabelPort": "Port", "LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)", "LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引", + "LabelPrimaryEbook": "Primary ebook", "LabelProgress": "进度", "LabelProvider": "供应商", "LabelPubDate": "出版日期", "LabelPublisher": "出版商", "LabelPublishYear": "发布年份", + "LabelRead": "Read", "LabelReadAgain": "Read Again", + "LabelReadEbookWithoutProgress": "Read ebook without keeping progress", "LabelRecentlyAdded": "最近添加", "LabelRecentSeries": "最近添加系列", "LabelRecommended": "推荐内容", @@ -366,6 +370,10 @@ "LabelSeries": "系列", "LabelSeriesName": "系列名称", "LabelSeriesProgress": "系列进度", + "LabelSetEbookAsPrimary": "Set as primary", + "LabelSetEbookAsSupplementary": "Set as supplementary", + "LabelSettingsAudiobooksOnly": "Audiobooks only", + "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计", "LabelSettingsChromecastSupport": "Chromecast 支持", "LabelSettingsDateFormat": "日期格式", @@ -489,6 +497,7 @@ "MessageChapterStartIsAfter": "章节开始是在有声读物结束之后", "MessageCheckingCron": "检查计划任务...", "MessageConfirmDeleteBackup": "你确定要删除备份 {0}?", + "MessageConfirmDeleteFile": "This will delete the file from your file system. Are you sure?", "MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?", "MessageConfirmDeleteSession": "你确定要删除此会话吗?", "MessageConfirmForceReScan": "你确定要强制重新扫描吗?", diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index c041fae7..5ccbceab 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -611,13 +611,26 @@ class LibraryItemController { } /** - * GET api/items/:id/ebook + * GET api/items/:id/ebook/:fileid? + * fileid is the inode value stored in LibraryFile.ino or EBookFile.ino + * fileid is only required when reading a supplementary ebook + * when no fileid is passed in the primary ebook will be returned * * @param {express.Request} req * @param {express.Response} res */ async getEBookFile(req, res) { - const ebookFile = req.libraryItem.media.ebookFile + let ebookFile = null + if (req.params.fileid) { + ebookFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid) + if (!ebookFile?.isEBookFile) { + Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) + return res.status(400).send('Invalid ebook file id') + } + } else { + ebookFile = req.libraryItem.media.ebookFile + } + if (!ebookFile) { Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.metadata.title}"`) return res.sendStatus(404) @@ -632,6 +645,37 @@ class LibraryItemController { res.sendFile(ebookFilePath) } + /** + * PATCH api/items/:id/ebook/:fileid/status + * toggle the status of an ebook file. + * if an ebook file is the primary ebook, then it will be changed to supplementary + * if an ebook file is supplementary, then it will be changed to primary + * + * @param {express.Request} req + * @param {express.Response} res + */ + async updateEbookFileStatus(req, res) { + const ebookLibraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid) + if (!ebookLibraryFile?.isEBookFile) { + Logger.error(`[LibraryItemController] Invalid ebook file id "${req.params.fileid}"`) + return res.status(400).send('Invalid ebook file id') + } + + if (ebookLibraryFile.isSupplementary) { + Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to primary`) + req.libraryItem.setPrimaryEbook(ebookLibraryFile) + } else { + Logger.info(`[LibraryItemController] Updating ebook file "${ebookLibraryFile.metadata.filename}" to supplementary`) + ebookLibraryFile.isSupplementary = true + req.libraryItem.setPrimaryEbook(null) + } + + req.libraryItem.updatedAt = Date.now() + await this.db.updateLibraryItem(req.libraryItem) + SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded()) + res.sendStatus(200) + } + middleware(req, res, next) { req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id) if (!req.libraryItem?.media) return res.sendStatus(404) diff --git a/server/objects/Library.js b/server/objects/Library.js index 719d4aa0..86e9f9de 100644 --- a/server/objects/Library.js +++ b/server/objects/Library.js @@ -33,6 +33,9 @@ class Library { get isMusic() { return this.mediaType === 'music' } + get isBook() { + return this.mediaType === 'book' + } construct(library) { this.id = library.id diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js index 2d1271ee..d473e399 100644 --- a/server/objects/LibraryItem.js +++ b/server/objects/LibraryItem.js @@ -80,6 +80,16 @@ class LibraryItem { this.media.libraryItemId = this.id this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f)) + + // Migration for v2.2.23 to set ebook library files as supplementary + if (this.isBook && this.media.ebookFile) { + for (const libraryFile of this.libraryFiles) { + if (libraryFile.isEBookFile && libraryFile.isSupplementary === null) { + libraryFile.isSupplementary = this.media.ebookFile.ino !== libraryFile.ino + } + } + } + } toJSON() { @@ -432,21 +442,30 @@ class LibraryItem { } // Set metadata from files - async syncFiles(preferOpfMetadata) { + async syncFiles(preferOpfMetadata, librarySettings) { let hasUpdated = false - if (this.mediaType === 'book') { - // Add/update ebook file (ebooks that were removed are removed in checkScanData) - if (this.media.ebookFile) { + if (this.isBook) { + // Add/update ebook files (ebooks that were removed are removed in checkScanData) + if (librarySettings.audiobooksOnly) { + hasUpdated = this.media.ebookFile + if (hasUpdated) { + // If library was set to audiobooks only then set primary ebook as supplementary + Logger.info(`[LibraryItem] Library is audiobooks only so setting ebook "${this.media.ebookFile.metadata.filename}" as supplementary`) + } + this.setPrimaryEbook(null) + } else if (this.media.ebookFile) { const matchingLibraryFile = this.libraryFiles.find(lf => lf.ino === this.media.ebookFile.ino) if (matchingLibraryFile && this.media.ebookFile.updateFromLibraryFile(matchingLibraryFile)) { hasUpdated = true } } else { + const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile && !lf.isSupplementary) + // Prefer epub ebook then fallback to first other ebook file - const ebookLibraryFile = this.libraryFiles.find(lf => lf.isEBookFile && lf.metadata.format === 'epub') || this.libraryFiles.find(lf => lf.isEBookFile) + const ebookLibraryFile = ebookLibraryFiles.find(lf => lf.metadata.format === 'epub') || ebookLibraryFiles[0] if (ebookLibraryFile) { - this.media.setEbookFile(ebookLibraryFile) + this.setPrimaryEbook(ebookLibraryFile) hasUpdated = true } } @@ -565,5 +584,20 @@ class LibraryItem { } return false } + + /** + * Set the EBookFile from a LibraryFile + * If null then ebookFile will be removed from the book + * all ebook library files that are not primary are marked as supplementary + * + * @param {LibraryFile} [libraryFile] + */ + setPrimaryEbook(ebookLibraryFile = null) { + const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile) + for (const libraryFile of ebookLibraryFiles) { + libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino + } + this.media.setEbookFile(ebookLibraryFile) + } } module.exports = LibraryItem \ No newline at end of file diff --git a/server/objects/files/LibraryFile.js b/server/objects/files/LibraryFile.js index d78bbaee..395e11cc 100644 --- a/server/objects/files/LibraryFile.js +++ b/server/objects/files/LibraryFile.js @@ -7,6 +7,7 @@ class LibraryFile { constructor(file) { this.ino = null this.metadata = null + this.isSupplementary = null this.addedAt = null this.updatedAt = null @@ -18,6 +19,7 @@ class LibraryFile { construct(file) { this.ino = file.ino this.metadata = new FileMetadata(file.metadata) + this.isSupplementary = file.isSupplementary === undefined ? null : file.isSupplementary this.addedAt = file.addedAt this.updatedAt = file.updatedAt } @@ -26,6 +28,7 @@ class LibraryFile { return { ino: this.ino, metadata: this.metadata.toJSON(), + isSupplementary: this.isSupplementary, addedAt: this.addedAt, updatedAt: this.updatedAt, fileType: this.fileType diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index c6e6b5e4..aea45e60 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -370,10 +370,20 @@ class Book { return payload } - setEbookFile(libraryFile) { - var ebookFile = new EBookFile() - ebookFile.setData(libraryFile) - this.ebookFile = ebookFile + /** + * Set the EBookFile from a LibraryFile + * If null then ebookFile will be removed from the book + * + * @param {LibraryFile} [libraryFile] + */ + setEbookFile(libraryFile = null) { + if (!libraryFile) { + this.ebookFile = null + } else { + const ebookFile = new EBookFile() + ebookFile.setData(libraryFile) + this.ebookFile = ebookFile + } } addAudioFile(audioFile) { diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js index 7ce66a5f..4b3df50d 100644 --- a/server/objects/settings/LibrarySettings.js +++ b/server/objects/settings/LibrarySettings.js @@ -7,6 +7,7 @@ class LibrarySettings { this.skipMatchingMediaWithAsin = false this.skipMatchingMediaWithIsbn = false this.autoScanCronExpression = null + this.audiobooksOnly = false if (settings) { this.construct(settings) @@ -19,6 +20,7 @@ class LibrarySettings { this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn this.autoScanCronExpression = settings.autoScanCronExpression || null + this.audiobooksOnly = !!settings.audiobooksOnly } toJSON() { @@ -27,12 +29,13 @@ class LibrarySettings { disableWatcher: this.disableWatcher, skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin, skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn, - autoScanCronExpression: this.autoScanCronExpression + autoScanCronExpression: this.autoScanCronExpression, + audiobooksOnly: this.audiobooksOnly } } update(payload) { - var hasUpdates = false + let hasUpdates = false for (const key in payload) { if (this[key] !== payload[key]) { this[key] = payload[key] diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index fbaf0d48..f41f62bd 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -125,7 +125,8 @@ class ApiRouter { this.router.get('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.getLibraryFile.bind(this)) this.router.delete('/items/:id/file/:fileid', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this)) this.router.get('/items/:id/file/:fileid/download', LibraryItemController.middleware.bind(this), LibraryItemController.downloadLibraryFile.bind(this)) - this.router.get('/items/:id/ebook', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this)) + this.router.get('/items/:id/ebook/:fileid?', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this)) + this.router.patch('/items/:id/ebook/:fileid/status', LibraryItemController.middleware.bind(this), LibraryItemController.updateEbookFileStatus.bind(this)) // // User Routes diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js index e533a6c3..6c9caa50 100644 --- a/server/scanner/LibraryScan.js +++ b/server/scanner/LibraryScan.js @@ -3,7 +3,7 @@ const fs = require('../libs/fsExtra') const date = require('../libs/dateAndTime') const Logger = require('../Logger') -const Folder = require('../objects/Folder') +const Library = require('../objects/Library') const { LogLevel } = require('../utils/constants') const filePerms = require('../utils/filePerms') const { getId, secondsToTimestamp } = require('../utils/index') @@ -12,10 +12,7 @@ class LibraryScan { constructor() { this.id = null this.type = null - this.libraryId = null - this.libraryName = null - this.libraryMediaType = null - this.folders = null + this.library = null this.verbose = false this.scanOptions = null @@ -31,6 +28,11 @@ class LibraryScan { this.logs = [] } + get libraryId() { return this.library.id } + get libraryName() { return this.library.name } + get libraryMediaType() { return this.library.mediaType } + get folders() { return this.library.folders } + get _scanOptions() { return this.scanOptions || {} } get forceRescan() { return !!this._scanOptions.forceRescan } get preferAudioMetadata() { return !!this._scanOptions.preferAudioMetadata } @@ -70,10 +72,7 @@ class LibraryScan { return { id: this.id, type: this.type, - libraryId: this.libraryId, - libraryName: this.libraryName, - libraryMediaType: this.libraryMediaType, - folders: this.folders.map(f => f.toJSON()), + library: this.library.toJSON(), scanOptions: this.scanOptions ? this.scanOptions.toJSON() : null, startedAt: this.startedAt, finishedAt: this.finishedAt, @@ -87,10 +86,7 @@ class LibraryScan { setData(library, scanOptions, type = 'scan') { this.id = getId('lscan') this.type = type - this.libraryId = library.id - this.libraryName = library.name - this.libraryMediaType = library.mediaType - this.folders = library.folders.map(folder => new Folder(folder.toJSON())) + this.library = new Library(library.toJSON()) // clone library this.scanOptions = scanOptions diff --git a/server/scanner/ScanOptions.js b/server/scanner/ScanOptions.js index 968f0723..873371bb 100644 --- a/server/scanner/ScanOptions.js +++ b/server/scanner/ScanOptions.js @@ -1,5 +1,5 @@ class ScanOptions { - constructor(options) { + constructor() { this.forceRescan = false // Server settings @@ -10,26 +10,11 @@ class ScanOptions { this.preferOpfMetadata = false this.preferMatchedMetadata = false this.preferOverdriveMediaMarker = false - - if (options) { - this.construct(options) - } - } - - construct(options) { - for (const key in options) { - if (key === 'metadataPrecedence' && options[key].length) { - this.metadataPrecedence = [...options[key]] - } else if (this[key] !== undefined) { - this[key] = options[key] - } - } } toJSON() { return { forceRescan: this.forceRescan, - metadataPrecedence: this.metadataPrecedence, parseSubtitles: this.parseSubtitles, findCovers: this.findCovers, storeCoverWithItem: this.storeCoverWithItem, diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 8e8333a4..b1846018 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -4,7 +4,7 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') // Utils -const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder } = require('../utils/scandir') +const { groupFilesIntoLibraryItemPaths, getLibraryItemFileData, scanFolder, checkFilepathIsAudioFile } = require('../utils/scandir') const { comparePaths } = require('../utils/index') const { getIno, filePathToPOSIX } = require('../utils/fileUtils') const { ScanResult, LogLevel } = require('../utils/constants') @@ -86,7 +86,7 @@ class Scanner { }) this.taskManager.addTask(task) - const result = await this.scanLibraryItem(library.mediaType, folder, libraryItem) + const result = await this.scanLibraryItem(library, folder, libraryItem) task.setFinished(this.getScanResultDescription(result)) this.taskManager.taskFinished(task) @@ -94,7 +94,9 @@ class Scanner { return result } - async scanLibraryItem(libraryMediaType, folder, libraryItem) { + async scanLibraryItem(library, folder, libraryItem) { + const libraryMediaType = library.mediaType + // TODO: Support for single media item const libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false) if (!libraryItemData) { @@ -106,7 +108,7 @@ class Scanner { if (checkRes.updated) hasUpdated = true // Sync other files first so that local images are used as cover art - if (await libraryItem.syncFiles(this.db.serverSettings.scannerPreferOpfMetadata)) { + if (await libraryItem.syncFiles(this.db.serverSettings.scannerPreferOpfMetadata, library.settings)) { hasUpdated = true } @@ -157,10 +159,10 @@ class Scanner { return } - var scanOptions = new ScanOptions() + const scanOptions = new ScanOptions() scanOptions.setData(options, this.db.serverSettings) - var libraryScan = new LibraryScan() + const libraryScan = new LibraryScan() libraryScan.setData(library, scanOptions) libraryScan.verbose = false this.librariesScanning.push(libraryScan.getScanEmitData) @@ -169,7 +171,7 @@ class Scanner { Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`) - var canceled = await this.scanLibrary(libraryScan) + const canceled = await this.scanLibrary(libraryScan) if (canceled) { Logger.info(`[Scanner] Library scan canceled for "${libraryScan.libraryName}"`) @@ -182,7 +184,7 @@ class Scanner { this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id) if (canceled && !libraryScan.totalResults) { - var emitData = libraryScan.getScanEmitData + const emitData = libraryScan.getScanEmitData emitData.results = null SocketAuthority.emitter('scan_complete', emitData) return @@ -201,7 +203,7 @@ class Scanner { // Scan each library for (let i = 0; i < libraryScan.folders.length; i++) { const folder = libraryScan.folders[i] - const itemDataFoundInFolder = await scanFolder(libraryScan.libraryMediaType, folder) + const itemDataFoundInFolder = await scanFolder(libraryScan.library, folder) libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`) libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder) } @@ -356,7 +358,7 @@ class Scanner { async scanNewLibraryItemDataChunk(newLibraryItemsData, libraryScan) { let newLibraryItems = await Promise.all(newLibraryItemsData.map((lid) => { - return this.scanNewLibraryItem(lid, libraryScan.libraryMediaType, libraryScan) + return this.scanNewLibraryItem(lid, libraryScan.library, libraryScan) })) newLibraryItems = newLibraryItems.filter(li => li) // Filter out nulls @@ -376,7 +378,7 @@ class Scanner { let hasUpdated = updated // Sync other files first to use local images as cover before extracting audio file cover - if (await libraryItem.syncFiles(libraryScan.preferOpfMetadata)) { + if (await libraryItem.syncFiles(libraryScan.preferOpfMetadata, libraryScan.library.settings)) { hasUpdated = true } @@ -425,7 +427,7 @@ class Scanner { return hasUpdated ? libraryItem : null } - async scanNewLibraryItem(libraryItemData, libraryMediaType, libraryScan = null) { + async scanNewLibraryItem(libraryItemData, library, libraryScan = null) { if (libraryScan) libraryScan.addLog(LogLevel.DEBUG, `Scanning new library item "${libraryItemData.path}"`) else Logger.debug(`[Scanner] Scanning new item "${libraryItemData.path}"`) @@ -433,14 +435,14 @@ class Scanner { const findCovers = libraryScan ? !!libraryScan.findCovers : !!global.ServerSettings.scannerFindCovers const libraryItem = new LibraryItem() - libraryItem.setData(libraryMediaType, libraryItemData) + libraryItem.setData(library.mediaType, libraryItemData) const mediaFiles = libraryItemData.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video') if (mediaFiles.length) { await MediaFileScanner.scanMediaFiles(mediaFiles, libraryItem, libraryScan) } - await libraryItem.syncFiles(preferOpfMetadata) + await libraryItem.syncFiles(preferOpfMetadata, library.settings) if (!libraryItem.hasMediaEntities) { Logger.warn(`[Scanner] Library item has no media files "${libraryItemData.path}"`) @@ -457,7 +459,7 @@ class Scanner { } // Scan for cover if enabled and has no cover - if (libraryMediaType === 'book') { + if (library.isBook) { if (libraryItem && findCovers && !libraryItem.media.coverPath && libraryItem.media.shouldSearchForCover) { const updatedCover = await this.searchForCover(libraryItem, libraryScan) libraryItem.media.updateLastCoverSearch(updatedCover) @@ -534,7 +536,7 @@ class Scanner { } async scanFilesChanged(fileUpdates) { - if (!fileUpdates || !fileUpdates.length) return + if (!fileUpdates?.length) return // If already scanning files from watcher then add these updates to queue if (this.scanningFilesChanged) { @@ -545,28 +547,28 @@ class Scanner { this.scanningFilesChanged = true // files grouped by folder - var folderGroups = this.getFileUpdatesGrouped(fileUpdates) + const folderGroups = this.getFileUpdatesGrouped(fileUpdates) for (const folderId in folderGroups) { - var libraryId = folderGroups[folderId].libraryId - var library = this.db.libraries.find(lib => lib.id === libraryId) + const libraryId = folderGroups[folderId].libraryId + const library = this.db.libraries.find(lib => lib.id === libraryId) if (!library) { Logger.error(`[Scanner] Library not found in files changed ${libraryId}`) continue; } - var folder = library.getFolderById(folderId) + const folder = library.getFolderById(folderId) if (!folder) { Logger.error(`[Scanner] Folder is not in library in files changed "${folderId}", Library "${library.name}"`) continue; } - var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath) - var fileUpdateGroup = groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths) + const relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath) + const fileUpdateGroup = groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths, false) if (!Object.keys(fileUpdateGroup).length) { Logger.info(`[Scanner] No important changes to scan for in folder "${folderId}"`) continue; } - var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup) + const folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup) Logger.debug(`[Scanner] Folder scan results`, folderScanResults) } @@ -584,25 +586,25 @@ class Scanner { // First pass - Remove files in parent dirs of items and remap the fileupdate group // Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item - var updateGroup = { ...fileUpdateGroup } + const updateGroup = { ...fileUpdateGroup } for (const itemDir in updateGroup) { if (itemDir == fileUpdateGroup[itemDir]) continue; // Media in root path - var itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/')) + const itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/')) if (!itemDirNestedFiles.length) continue; - var firstNest = itemDirNestedFiles[0].split('/').shift() - var altDir = `${itemDir}/${firstNest}` + const firstNest = itemDirNestedFiles[0].split('/').shift() + const altDir = `${itemDir}/${firstNest}` - var fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir) - var childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath)) + const fullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), itemDir) + const childLibraryItem = this.db.libraryItems.find(li => li.path !== fullPath && li.path.startsWith(fullPath)) if (!childLibraryItem) { - continue; + continue } - var altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir) - var altChildLibraryItem = this.db.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath)) + const altFullPath = Path.posix.join(filePathToPOSIX(folder.fullPath), altDir) + const altChildLibraryItem = this.db.libraryItems.find(li => li.path !== altFullPath && li.path.startsWith(altFullPath)) if (altChildLibraryItem) { - continue; + continue } delete fileUpdateGroup[itemDir] @@ -638,14 +640,17 @@ class Scanner { SocketAuthority.emitter('item_updated', existingLibraryItem.toJSONExpanded()) itemGroupingResults[itemDir] = ScanResult.REMOVED - continue; + continue } } // Scan library item for updates Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) - itemGroupingResults[itemDir] = await this.scanLibraryItem(library.mediaType, folder, existingLibraryItem) - continue; + itemGroupingResults[itemDir] = await this.scanLibraryItem(library, folder, existingLibraryItem) + continue + } else if (library.settings.audiobooksOnly && !fileUpdateGroup[itemDir].some(checkFilepathIsAudioFile)) { + Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" has no audio files`) + continue } // Check if a library item is a subdirectory of this dir @@ -653,12 +658,12 @@ class Scanner { if (childItem) { Logger.warn(`[Scanner] Files were modified in a parent directory of a library item "${childItem.media.metadata.title}" - ignoring`) itemGroupingResults[itemDir] = ScanResult.NOTHING - continue; + continue } Logger.debug(`[Scanner] Folder update group must be a new item "${itemDir}" in library "${library.name}"`) var isSingleMediaItem = itemDir === fileUpdateGroup[itemDir] - var newLibraryItem = await this.scanPotentialNewLibraryItem(library.mediaType, folder, fullPath, isSingleMediaItem) + var newLibraryItem = await this.scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem) if (newLibraryItem) { await this.createNewAuthorsAndSeries(newLibraryItem) await this.db.insertLibraryItem(newLibraryItem) @@ -670,10 +675,10 @@ class Scanner { return itemGroupingResults } - async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath, isSingleMediaItem = false) { - const libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem) + async scanPotentialNewLibraryItem(library, folder, fullPath, isSingleMediaItem = false) { + const libraryItemData = await getLibraryItemFileData(library.mediaType, folder, fullPath, isSingleMediaItem) if (!libraryItemData) return null - return this.scanNewLibraryItem(libraryItemData, libraryMediaType) + return this.scanNewLibraryItem(libraryItemData, library) } async searchForCover(libraryItem, libraryScan = null) { diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 7f5b9855..30a6406a 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -5,14 +5,23 @@ const { recurseFiles, getFileTimestampsWithIno, filePathToPOSIX } = require('./f const globals = require('./globals') const LibraryFile = require('../objects/files/LibraryFile') -function isMediaFile(mediaType, ext) { +function isMediaFile(mediaType, ext, audiobooksOnly = false) { if (!ext) return false - var extclean = ext.slice(1).toLowerCase() + const extclean = ext.slice(1).toLowerCase() if (mediaType === 'podcast' || mediaType === 'music') return globals.SupportedAudioTypes.includes(extclean) else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean) + else if (audiobooksOnly) return globals.SupportedAudioTypes.includes(extclean) return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean) } +function checkFilepathIsAudioFile(filepath) { + const ext = Path.extname(filepath) + if (!ext) return false + const extclean = ext.slice(1).toLowerCase() + return globals.SupportedAudioTypes.includes(extclean) +} +module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile + // TODO: Function needs to be re-done // Input: array of relative file paths // Output: map of files grouped into potential item dirs @@ -25,12 +34,12 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { let parsedPath = Path.parse(path) // Is not in root dir OR is a book media file if (parsedPath.dir) { - if (!isMediaFile(mediaType, parsedPath.ext)) { // Seperate out non-media files + if (!isMediaFile(mediaType, parsedPath.ext, false)) { // Seperate out non-media files nonMediaFilePaths.push(path) return false } return true - } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext)) { // (book media type supports single file audiobooks/ebooks in root dir) + } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) { // (book media type supports single file audiobooks/ebooks in root dir) return true } return false @@ -90,11 +99,11 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths // Input: array of relative file items (see recurseFiles) // Output: map of files grouped into potential libarary item dirs -function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { +function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) { // Handle music where every audio file is a library item if (mediaType === 'music') { const audioFileGroup = {} - fileItems.filter(i => isMediaFile(mediaType, i.extension)).forEach((item) => { + fileItems.filter(i => isMediaFile(mediaType, i.extension, audiobooksOnly)).forEach((item) => { audioFileGroup[item.path] = item.path }) return audioFileGroup @@ -102,7 +111,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { // Step 1: Filter out non-book-media files in root dir (with depth of 0) const itemsFiltered = fileItems.filter(i => { - return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension)) + return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension, audiobooksOnly)) }) // Step 2: Seperate media files and other files @@ -110,7 +119,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { const mediaFileItems = [] const otherFileItems = [] itemsFiltered.forEach(item => { - if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item) + if (isMediaFile(mediaType, item.extension, audiobooksOnly)) mediaFileItems.push(item) else otherFileItems.push(item) }) @@ -175,7 +184,7 @@ function cleanFileObjects(libraryItemPath, files) { } // Scan folder -async function scanFolder(libraryMediaType, folder) { +async function scanFolder(library, folder) { const folderPath = filePathToPOSIX(folder.fullPath) const pathExists = await fs.pathExists(folderPath) @@ -185,7 +194,7 @@ async function scanFolder(libraryMediaType, folder) { } const fileItems = await recurseFiles(folderPath) - const libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems) + const libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(library.mediaType, fileItems, library.settings.audiobooksOnly) if (!Object.keys(libraryItemGrouping).length) { Logger.error(`Root path has no media folders: ${folderPath}`) @@ -197,7 +206,7 @@ async function scanFolder(libraryMediaType, folder) { let isFile = false // item is not in a folder let libraryItemData = null let fileObjs = [] - if (libraryMediaType === 'music') { + if (library.mediaType === 'music') { libraryItemData = { path: Path.posix.join(folderPath, libraryItemPath), relPath: libraryItemPath @@ -216,7 +225,7 @@ async function scanFolder(libraryMediaType, folder) { fileObjs = await cleanFileObjects(folderPath, [libraryItemPath]) isFile = true } else { - libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath) + libraryItemData = getDataFromMediaDir(library.mediaType, folderPath, libraryItemPath) fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath]) }