diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue index bf2423bf..5d91daa6 100644 --- a/client/components/modals/libraries/LibrarySettings.vue +++ b/client/components/modals/libraries/LibrarySettings.vue @@ -38,6 +38,17 @@

{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}

+
+
+ + +

+ {{ $strings.LabelSettingsHideSingleBookSeries }} + info_outlined +

+
+
+
@@ -57,7 +68,8 @@ export default { disableWatcher: false, skipMatchingMediaWithAsin: false, skipMatchingMediaWithIsbn: false, - audiobooksOnly: false + audiobooksOnly: false, + hideSingleBookSeries: false } }, computed: { @@ -86,7 +98,8 @@ export default { disableWatcher: !!this.disableWatcher, skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin, skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn, - audiobooksOnly: !!this.audiobooksOnly + audiobooksOnly: !!this.audiobooksOnly, + hideSingleBookSeries: !!this.hideSingleBookSeries } } }, @@ -99,6 +112,7 @@ export default { this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn this.audiobooksOnly = !!this.librarySettings.audiobooksOnly + this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries } }, mounted() { diff --git a/client/strings/en-us.json b/client/strings/en-us.json index be8ac1ff..36fdec68 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -395,6 +395,8 @@ "LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.", "LabelSettingsFindCovers": "Find covers", "LabelSettingsFindCoversHelp": "If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.
Note: This will extend scan time", + "LabelSettingsHideSingleBookSeries": "Hide single book series", + "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsOverdriveMediaMarkers": "Use Overdrive Media Markers for chapters", diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index e52c6652..55f2c79f 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -209,7 +209,7 @@ class LibraryController { // If also filtering by series, will not collapse the filtered series as this would lead // to series having a collapsed series that is just that series. if (payload.collapseseries) { - let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries) + let collapsedItems = libraryHelpers.collapseBookSeries(libraryItems, this.db.series, filterSeries, req.library.settings.hideSingleBookSeries) if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) { libraryItems = collapsedItems @@ -405,7 +405,7 @@ class LibraryController { include: include.join(',') } - let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified) + let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified, req.library.settings.hideSingleBookSeries) const direction = payload.sortDesc ? 'desc' : 'asc' series = naturalSort(series).by([ @@ -544,12 +544,10 @@ class LibraryController { // api/libraries/:id/personalized // New and improved personalized call only loops through library items once async getLibraryUserPersonalizedOptimal(req, res) { - const mediaType = req.library.mediaType - const libraryItems = req.libraryItems const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v) - const categories = libraryHelpers.buildPersonalizedShelves(this, req.user, libraryItems, mediaType, limitPerShelf, include) + const categories = libraryHelpers.buildPersonalizedShelves(this, req.user, req.libraryItems, req.library, limitPerShelf, include) res.json(categories) } diff --git a/server/objects/settings/LibrarySettings.js b/server/objects/settings/LibrarySettings.js index 4b3df50d..79c729ef 100644 --- a/server/objects/settings/LibrarySettings.js +++ b/server/objects/settings/LibrarySettings.js @@ -8,6 +8,7 @@ class LibrarySettings { this.skipMatchingMediaWithIsbn = false this.autoScanCronExpression = null this.audiobooksOnly = false + this.hideSingleBookSeries = false // Do not show series that only have 1 book if (settings) { this.construct(settings) @@ -21,6 +22,7 @@ class LibrarySettings { this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn this.autoScanCronExpression = settings.autoScanCronExpression || null this.audiobooksOnly = !!settings.audiobooksOnly + this.hideSingleBookSeries = !!settings.hideSingleBookSeries } toJSON() { @@ -30,7 +32,8 @@ class LibrarySettings { skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin, skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn, autoScanCronExpression: this.autoScanCronExpression, - audiobooksOnly: this.audiobooksOnly + audiobooksOnly: this.audiobooksOnly, + hideSingleBookSeries: this.hideSingleBookSeries } } diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 2b9a56c0..899e43bb 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -168,7 +168,7 @@ module.exports = { return data }, - getSeriesFromBooks(books, allSeries, filterSeries, filterBy, user, minified = false) { + getSeriesFromBooks(books, allSeries, filterSeries, filterBy, user, minified, hideSingleBookSeries) { const _series = {} const seriesToFilterOut = {} books.forEach((libraryItem) => { @@ -218,6 +218,11 @@ module.exports = { let seriesItems = Object.values(_series) + // Library setting to hide series with only 1 book + if (hideSingleBookSeries) { + seriesItems = seriesItems.filter(se => se.books.length > 1) + } + // check progress filter if (filterBy && filterBy.startsWith('progress.') && user) { seriesItems = seriesItems.filter(se => this.checkSeriesProgressFilter(se, filterBy, user)) @@ -312,11 +317,11 @@ module.exports = { }, - collapseBookSeries(libraryItems, series, filterSeries) { + collapseBookSeries(libraryItems, series, filterSeries, hideSingleBookSeries) { // Get series from the library items. If this list is being collapsed after filtering for a series, // don't collapse that series, only books that are in other series. const seriesObjects = this - .getSeriesFromBooks(libraryItems, series, filterSeries, null, null, true) + .getSeriesFromBooks(libraryItems, series, filterSeries, null, null, true, hideSingleBookSeries) .filter(s => s.id != filterSeries) const filteredLibraryItems = [] @@ -341,9 +346,11 @@ module.exports = { return filteredLibraryItems }, - buildPersonalizedShelves(ctx, user, libraryItems, mediaType, maxEntitiesPerShelf, include) { + buildPersonalizedShelves(ctx, user, libraryItems, library, maxEntitiesPerShelf, include) { + const mediaType = library.mediaType const isPodcastLibrary = mediaType === 'podcast' const includeRssFeed = include.includes('rssfeed') + const hideSingleBookSeries = library.settings.hideSingleBookSeries const shelves = [ { @@ -580,21 +587,11 @@ module.exports = { } seriesMap[librarySeries.id] = series - if (series.addedAt > categoryMap['recent-series'].smallest) { - const indexToPut = categoryMap['recent-series'].items.findIndex(i => series.addedAt > i.addedAt) - if (indexToPut >= 0) { - categoryMap['recent-series'].items.splice(indexToPut, 0, series) - } else { - categoryMap['recent-series'].items.push(series) - } - - // Max series is 5 - if (categoryMap['recent-series'].items.length > 5) { - categoryMap['recent-series'].items.pop() - categoryMap['recent-series'].smallest = categoryMap['recent-series'].items[categoryMap['recent-series'].items.length - 1].addedAt - } - - categoryMap['recent-series'].biggest = categoryMap['recent-series'].items[0].addedAt + const indexToPut = categoryMap['recent-series'].items.findIndex(i => series.addedAt > i.addedAt) + if (indexToPut >= 0) { + categoryMap['recent-series'].items.splice(indexToPut, 0, series) + } else { + categoryMap['recent-series'].items.push(series) } } } else { @@ -819,6 +816,12 @@ module.exports = { // Sort series books by sequence if (categoryMap['recent-series'].items.length) { + if (hideSingleBookSeries) { + categoryMap['recent-series'].items = categoryMap['recent-series'].items.filter(seriesItem => seriesItem.books.length > 1) + } + // Limit series shown to 5 + categoryMap['recent-series'].items = categoryMap['recent-series'].items.slice(0, 5) + for (const seriesItem of categoryMap['recent-series'].items) { seriesItem.books = naturalSort(seriesItem.books).asc(li => li.seriesSequence) }