From d5e00c8bbd88019457644f6bcb09a41251f81215 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 1 Jul 2024 17:26:13 -0500 Subject: [PATCH] Update:Get personalized home page shelves and get library items endpoint optional includes for media item shares, show public icon on shared book items --- .../components/app/BookShelfCategorized.vue | 2 +- client/components/app/LazyBookshelf.vue | 2 +- client/components/cards/LazyBookCard.vue | 8 + server/controllers/LibraryItemController.js | 2 +- server/models/LibraryItem.js | 8 +- server/utils/queries/libraryFilters.js | 167 ++++++++++-------- .../utils/queries/libraryItemsBookFilters.js | 7 + 7 files changed, 117 insertions(+), 79 deletions(-) diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index f01e829e..ff0c5ff7 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -168,7 +168,7 @@ export default { }, async fetchCategories() { const categories = await this.$axios - .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`) + .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`) .then((data) => { return data }) diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index bb088b84..fa47ce99 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -312,7 +312,7 @@ export default { let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : '' - const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete` + const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete,share` const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => { console.error('failed to fetch items', error) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 5357267b..426a43d0 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -91,9 +91,14 @@ +
rss_feed
+ +
+ public +
@@ -627,6 +632,9 @@ export default { rssFeed() { if (this.booksInSeries) return null return this._libraryItem.rssFeed || null + }, + mediaItemShare() { + return this._libraryItem.mediaItemShare || null } }, methods: { diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 32b098ed..3f72603f 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -21,7 +21,7 @@ class LibraryItemController { /** * GET: /api/items/:id * Optional query params: - * ?include=progress,rssfeed,downloads + * ?include=progress,rssfeed,downloads,share * ?expanded=1 * * @param {import('express').Request} req diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 2eccee19..cffcb80a 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -10,6 +10,8 @@ const LibraryFile = require('../objects/files/LibraryFile') const Book = require('./Book') const Podcast = require('./Podcast') +const ShareManager = require('../managers/ShareManager') + /** * @typedef LibraryFileObject * @property {string} ino @@ -537,7 +539,7 @@ class LibraryItem extends Model { * @param {oldLibrary} library * @param {oldUser} user * @param {object} options - * @returns {object} { libraryItems:oldLibraryItem[], count:number } + * @returns {{ libraryItems:oldLibraryItem[], count:number }} */ static async getByFilterAndSort(library, user, options) { let start = Date.now() @@ -565,6 +567,10 @@ class LibraryItem extends Model { if (li.numEpisodesIncomplete) { oldLibraryItem.numEpisodesIncomplete = li.numEpisodesIncomplete } + if (li.mediaType === 'book' && options.include?.includes?.('share')) { + console.log('Lookup share for media item id', li.mediaId) + oldLibraryItem.mediaItemShare = ShareManager.findByMediaItemId(li.mediaId) + } return oldLibraryItem }), diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index a2efb664..ffcbd83e 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -15,9 +15,9 @@ module.exports = { /** * Get library items using filter and sort - * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user - * @param {object} options + * @param {import('../../objects/Library')} library + * @param {import('../../objects/user/User')} user + * @param {object} options * @returns {object} { libraryItems:LibraryItem[], count:number } */ async getFilteredLibraryItems(library, user, options) { @@ -27,7 +27,7 @@ module.exports = { let filterGroup = null if (filterBy) { const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks'] - const group = searchGroups.find(_group => filterBy.startsWith(_group + '.')) + const group = searchGroups.find((_group) => filterBy.startsWith(_group + '.')) filterGroup = group || filterBy filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null } @@ -41,21 +41,24 @@ module.exports = { /** * Get library items for continue listening & continue reading shelves - * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user - * @param {string[]} include - * @param {number} limit + * @param {import('../../objects/Library')} library + * @param {import('../../objects/user/User')} user + * @param {string[]} include + * @param {number} limit * @returns {Promise<{ items:import('../../models/LibraryItem')[], count:number }>} */ async getMediaItemsInProgress(library, user, include, limit) { if (library.mediaType === 'book') { const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0, true) return { - items: libraryItems.map(li => { + items: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() } + if (li.mediaItemShare) { + oldLibraryItem.mediaItemShare = li.mediaItemShare + } return oldLibraryItem }), count @@ -64,7 +67,7 @@ module.exports = { const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'in-progress', 'progress', true, limit, 0, true) return { count, - items: libraryItems.map(li => { + items: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() oldLibraryItem.recentEpisode = li.recentEpisode return oldLibraryItem @@ -75,17 +78,17 @@ module.exports = { /** * Get library items for most recently added shelf - * @param {import('../../objects/Library')} library - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit + * @param {import('../../objects/Library')} library + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit * @returns {object} { libraryItems:LibraryItem[], count:number } */ async getLibraryItemsMostRecentlyAdded(library, user, include, limit) { if (library.mediaType === 'book') { const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0) return { - libraryItems: libraryItems.map(li => { + libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() @@ -93,6 +96,9 @@ module.exports = { if (li.size && !oldLibraryItem.media.size) { oldLibraryItem.media.size = li.size } + if (li.mediaItemShare) { + oldLibraryItem.mediaItemShare = li.mediaItemShare + } return oldLibraryItem }), count @@ -100,7 +106,7 @@ module.exports = { } else { const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, include, limit, 0) return { - libraryItems: libraryItems.map(li => { + libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() @@ -120,16 +126,16 @@ module.exports = { /** * Get library items for continue series shelf - * @param {import('../../objects/Library')} library - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit + * @param {import('../../objects/Library')} library + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit * @returns {object} { libraryItems:LibraryItem[], count:number } */ async getLibraryItemsContinueSeries(library, user, include, limit) { const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library, user, include, limit, 0) return { - libraryItems: libraryItems.map(li => { + libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() @@ -137,6 +143,9 @@ module.exports = { if (li.series) { oldLibraryItem.media.metadata.series = li.series } + if (li.mediaItemShare) { + oldLibraryItem.mediaItemShare = li.mediaItemShare + } return oldLibraryItem }), count @@ -145,21 +154,24 @@ module.exports = { /** * Get library items or podcast episodes for the "Listen Again" and "Read Again" shelf - * @param {import('../../objects/Library')} library - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit + * @param {import('../../objects/Library')} library + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit * @returns {object} { items:object[], count:number } */ async getMediaFinished(library, user, include, limit) { if (library.mediaType === 'book') { const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0) return { - items: libraryItems.map(li => { + items: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() } + if (li.mediaItemShare) { + oldLibraryItem.mediaItemShare = li.mediaItemShare + } return oldLibraryItem }), count @@ -168,7 +180,7 @@ module.exports = { const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'finished', 'progress', true, limit, 0) return { count, - items: libraryItems.map(li => { + items: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() oldLibraryItem.recentEpisode = li.recentEpisode return oldLibraryItem @@ -179,11 +191,11 @@ module.exports = { /** * Get series for recent series shelf - * @param {import('../../objects/Library')} library + * @param {import('../../objects/Library')} library * @param {import('../../objects/user/User')} user - * @param {string[]} include - * @param {number} limit - * @returns {{ series:import('../../objects/entities/Series')[], count:number}} + * @param {string[]} include + * @param {number} limit + * @returns {{ series:import('../../objects/entities/Series')[], count:number}} */ async getSeriesMostRecentlyAdded(library, user, include, limit) { if (!library.isBook) return { series: [], count: 0 } @@ -201,7 +213,7 @@ module.exports = { { libraryId: library.id, createdAt: { - [Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago + [Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago } } ] @@ -209,9 +221,11 @@ module.exports = { // Handle library setting to hide single book series // TODO: Merge with existing query if (library.settings.hideSingleBookSeries) { - seriesWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id)`), { - [Sequelize.Op.gt]: 1 - })) + seriesWhere.push( + Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id)`), { + [Sequelize.Op.gt]: 1 + }) + ) } // Handle user permissions to only include series with at least 1 book @@ -228,9 +242,11 @@ module.exports = { attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) > 0' } } - seriesWhere.push(Sequelize.where(Sequelize.literal(`(${attrQuery})`), { - [Sequelize.Op.gt]: 0 - })) + seriesWhere.push( + Sequelize.where(Sequelize.literal(`(${attrQuery})`), { + [Sequelize.Op.gt]: 0 + }) + ) } const { rows: series, count } = await Database.seriesModel.findAndCountAll({ @@ -254,9 +270,7 @@ module.exports = { }, ...seriesIncludes ], - order: [ - ['createdAt', 'DESC'] - ] + order: [['createdAt', 'DESC']] }) const allOldSeries = [] @@ -276,18 +290,20 @@ module.exports = { sensitivity: 'base' }) }) - oldSeries.books = s.bookSeries.map(bs => { - const libraryItem = bs.book.libraryItem?.toJSON() - if (!libraryItem) { - Logger.warn(`Book series book has no libraryItem`, bs, bs.book, 'series=', series) - return null - } + oldSeries.books = s.bookSeries + .map((bs) => { + const libraryItem = bs.book.libraryItem?.toJSON() + if (!libraryItem) { + Logger.warn(`Book series book has no libraryItem`, bs, bs.book, 'series=', series) + return null + } - delete bs.book.libraryItem - libraryItem.media = bs.book - const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified() - return oldLibraryItem - }).filter(b => b) + delete bs.book.libraryItem + libraryItem.media = bs.book + const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified() + return oldLibraryItem + }) + .filter((b) => b) allOldSeries.push(oldSeries) } @@ -300,9 +316,9 @@ module.exports = { /** * Get most recently created authors for "Newest Authors" shelf * Author must be linked to at least 1 book - * @param {oldLibrary} library + * @param {oldLibrary} library * @param {oldUser} user - * @param {number} limit + * @param {number} limit * @returns {object} { authors:oldAuthor[], count:number } */ async getNewestAuthors(library, user, limit) { @@ -314,7 +330,7 @@ module.exports = { where: { libraryId: library.id, createdAt: { - [Sequelize.Op.gte]: new Date(new Date() - (60 * 24 * 60 * 60 * 1000)) // 60 days ago + [Sequelize.Op.gte]: new Date(new Date() - 60 * 24 * 60 * 60 * 1000) // 60 days ago } }, replacements, @@ -329,9 +345,7 @@ module.exports = { }, limit, distinct: true, - order: [ - ['createdAt', 'DESC'] - ] + order: [['createdAt', 'DESC']] }) return { @@ -345,10 +359,10 @@ module.exports = { /** * Get book library items for the "Discover" shelf - * @param {oldLibrary} library - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit + * @param {oldLibrary} library + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit * @returns {object} {libraryItems:oldLibraryItem[], count:number} */ async getLibraryItemsToDiscover(library, user, include, limit) { @@ -356,11 +370,14 @@ module.exports = { const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit) return { - libraryItems: libraryItems.map(li => { + libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() if (li.rssFeed) { oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() } + if (li.mediaItemShare) { + oldLibraryItem.mediaItemShare = li.mediaItemShare + } return oldLibraryItem }), count @@ -369,9 +386,9 @@ module.exports = { /** * Get podcast episodes most recently added - * @param {oldLibrary} library - * @param {oldUser} user - * @param {number} limit + * @param {oldLibrary} library + * @param {oldUser} user + * @param {number} limit * @returns {object} {libraryItems:oldLibraryItem[], count:number} */ async getNewestPodcastEpisodes(library, user, limit) { @@ -380,7 +397,7 @@ module.exports = { const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'recent', null, 'createdAt', true, limit, 0) return { count, - libraryItems: libraryItems.map(li => { + libraryItems: libraryItems.map((li) => { const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() oldLibraryItem.recentEpisode = li.recentEpisode return oldLibraryItem @@ -390,10 +407,10 @@ module.exports = { /** * Get library items for an author, optional use user permissions - * @param {oldAuthor} author - * @param {[oldUser]} user - * @param {number} limit - * @param {number} offset + * @param {oldAuthor} author + * @param {[oldUser]} user + * @param {number} limit + * @param {number} offset * @returns {Promise} { libraryItems:LibraryItem[], count:number } */ async getLibraryItemsForAuthor(author, user, limit, offset) { @@ -406,7 +423,7 @@ module.exports = { /** * Get book library items in a collection - * @param {oldCollection} collection + * @param {oldCollection} collection * @returns {Promise} */ getLibraryItemsForCollection(collection) { @@ -415,7 +432,7 @@ module.exports = { /** * Get filter data used in filter menus - * @param {string} mediaType + * @param {string} mediaType * @param {string} libraryId * @returns {Promise} */ @@ -507,10 +524,10 @@ module.exports = { authors.forEach((a) => data.authors.push({ id: a.id, name: a.name })) } - data.authors = naturalSort(data.authors).asc(au => au.name) + data.authors = naturalSort(data.authors).asc((au) => au.name) data.genres = naturalSort([...data.genres]).asc() data.tags = naturalSort([...data.tags]).asc() - data.series = naturalSort(data.series).asc(se => se.name) + data.series = naturalSort(data.series).asc((se) => se.name) data.narrators = naturalSort([...data.narrators]).asc() data.publishers = naturalSort([...data.publishers]).asc() data.languages = naturalSort([...data.languages]).asc() diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index f88c6385..87374d54 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -4,6 +4,8 @@ const Logger = require('../../Logger') const authorFilters = require('./authorFilters') const { asciiOnlyToLowerCase } = require('../index') +const ShareManager = require('../../managers/ShareManager') + module.exports = { /** * User permissions to restrict books for explicit content & tags @@ -354,6 +356,7 @@ module.exports = { sortBy = 'media.metadata.title' } const includeRSSFeed = include.includes('rssfeed') + const includeMediaItemShare = include.includes('share') // For sorting by author name an additional attribute must be added // with author names concatenated @@ -605,6 +608,10 @@ module.exports = { libraryItem.rssFeed = libraryItem.feeds[0] } + if (includeMediaItemShare) { + libraryItem.mediaItemShare = ShareManager.findByMediaItemId(libraryItem.mediaId) + } + libraryItem.media = book return libraryItem