Update:Get personalized home page shelves and get library items endpoint optional includes for media item shares, show public icon on shared book items

This commit is contained in:
advplyr 2024-07-01 17:26:13 -05:00
parent 134c2580c9
commit d5e00c8bbd
7 changed files with 117 additions and 79 deletions

View File

@ -168,7 +168,7 @@ export default {
}, },
async fetchCategories() { async fetchCategories() {
const categories = await this.$axios 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) => { .then((data) => {
return data return data
}) })

View File

@ -312,7 +312,7 @@ export default {
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : '' 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) => { const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
console.error('failed to fetch items', error) console.error('failed to fetch items', error)

View File

@ -91,9 +91,14 @@
</div> </div>
</ui-tooltip> </ui-tooltip>
<!-- rss feed icon -->
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }"> <div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span> <span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
</div> </div>
<!-- media item shared icon -->
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">public</span>
</div>
<!-- Series sequence --> <!-- Series sequence -->
<div cy-id="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }"> <div cy-id="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
@ -627,6 +632,9 @@ export default {
rssFeed() { rssFeed() {
if (this.booksInSeries) return null if (this.booksInSeries) return null
return this._libraryItem.rssFeed || null return this._libraryItem.rssFeed || null
},
mediaItemShare() {
return this._libraryItem.mediaItemShare || null
} }
}, },
methods: { methods: {

View File

@ -21,7 +21,7 @@ class LibraryItemController {
/** /**
* GET: /api/items/:id * GET: /api/items/:id
* Optional query params: * Optional query params:
* ?include=progress,rssfeed,downloads * ?include=progress,rssfeed,downloads,share
* ?expanded=1 * ?expanded=1
* *
* @param {import('express').Request} req * @param {import('express').Request} req

View File

@ -10,6 +10,8 @@ const LibraryFile = require('../objects/files/LibraryFile')
const Book = require('./Book') const Book = require('./Book')
const Podcast = require('./Podcast') const Podcast = require('./Podcast')
const ShareManager = require('../managers/ShareManager')
/** /**
* @typedef LibraryFileObject * @typedef LibraryFileObject
* @property {string} ino * @property {string} ino
@ -537,7 +539,7 @@ class LibraryItem extends Model {
* @param {oldLibrary} library * @param {oldLibrary} library
* @param {oldUser} user * @param {oldUser} user
* @param {object} options * @param {object} options
* @returns {object} { libraryItems:oldLibraryItem[], count:number } * @returns {{ libraryItems:oldLibraryItem[], count:number }}
*/ */
static async getByFilterAndSort(library, user, options) { static async getByFilterAndSort(library, user, options) {
let start = Date.now() let start = Date.now()
@ -565,6 +567,10 @@ class LibraryItem extends Model {
if (li.numEpisodesIncomplete) { if (li.numEpisodesIncomplete) {
oldLibraryItem.numEpisodesIncomplete = 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 return oldLibraryItem
}), }),

View File

@ -27,7 +27,7 @@ module.exports = {
let filterGroup = null let filterGroup = null
if (filterBy) { if (filterBy) {
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks'] 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 filterGroup = group || filterBy
filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null filterValue = group ? this.decode(filterBy.replace(`${group}.`, '')) : null
} }
@ -51,11 +51,14 @@ module.exports = {
if (library.mediaType === 'book') { if (library.mediaType === 'book') {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0, true) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'in-progress', 'progress', true, false, include, limit, 0, true)
return { return {
items: libraryItems.map(li => { items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.mediaItemShare) {
oldLibraryItem.mediaItemShare = li.mediaItemShare
}
return oldLibraryItem return oldLibraryItem
}), }),
count count
@ -64,7 +67,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'in-progress', 'progress', true, limit, 0, true) const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'in-progress', 'progress', true, limit, 0, true)
return { return {
count, count,
items: libraryItems.map(li => { items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem return oldLibraryItem
@ -85,7 +88,7 @@ module.exports = {
if (library.mediaType === 'book') { if (library.mediaType === 'book') {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, false, include, limit, 0)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
@ -93,6 +96,9 @@ module.exports = {
if (li.size && !oldLibraryItem.media.size) { if (li.size && !oldLibraryItem.media.size) {
oldLibraryItem.media.size = li.size oldLibraryItem.media.size = li.size
} }
if (li.mediaItemShare) {
oldLibraryItem.mediaItemShare = li.mediaItemShare
}
return oldLibraryItem return oldLibraryItem
}), }),
count count
@ -100,7 +106,7 @@ module.exports = {
} else { } else {
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, include, limit, 0) const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, 'recent', null, 'addedAt', true, include, limit, 0)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
@ -129,7 +135,7 @@ module.exports = {
async getLibraryItemsContinueSeries(library, user, include, limit) { async getLibraryItemsContinueSeries(library, user, include, limit) {
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library, user, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library, user, include, limit, 0)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
@ -137,6 +143,9 @@ module.exports = {
if (li.series) { if (li.series) {
oldLibraryItem.media.metadata.series = li.series oldLibraryItem.media.metadata.series = li.series
} }
if (li.mediaItemShare) {
oldLibraryItem.mediaItemShare = li.mediaItemShare
}
return oldLibraryItem return oldLibraryItem
}), }),
count count
@ -155,11 +164,14 @@ module.exports = {
if (library.mediaType === 'book') { if (library.mediaType === 'book') {
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0) const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', 'finished', 'progress', true, false, include, limit, 0)
return { return {
items: libraryItems.map(li => { items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.mediaItemShare) {
oldLibraryItem.mediaItemShare = li.mediaItemShare
}
return oldLibraryItem return oldLibraryItem
}), }),
count count
@ -168,7 +180,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'finished', 'progress', true, limit, 0) const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'finished', 'progress', true, limit, 0)
return { return {
count, count,
items: libraryItems.map(li => { items: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem return oldLibraryItem
@ -201,7 +213,7 @@ module.exports = {
{ {
libraryId: library.id, libraryId: library.id,
createdAt: { 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 // Handle library setting to hide single book series
// TODO: Merge with existing query // TODO: Merge with existing query
if (library.settings.hideSingleBookSeries) { 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)`), { 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 [Sequelize.Op.gt]: 1
})) })
)
} }
// Handle user permissions to only include series with at least 1 book // 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' 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})`), { seriesWhere.push(
Sequelize.where(Sequelize.literal(`(${attrQuery})`), {
[Sequelize.Op.gt]: 0 [Sequelize.Op.gt]: 0
})) })
)
} }
const { rows: series, count } = await Database.seriesModel.findAndCountAll({ const { rows: series, count } = await Database.seriesModel.findAndCountAll({
@ -254,9 +270,7 @@ module.exports = {
}, },
...seriesIncludes ...seriesIncludes
], ],
order: [ order: [['createdAt', 'DESC']]
['createdAt', 'DESC']
]
}) })
const allOldSeries = [] const allOldSeries = []
@ -276,7 +290,8 @@ module.exports = {
sensitivity: 'base' sensitivity: 'base'
}) })
}) })
oldSeries.books = s.bookSeries.map(bs => { oldSeries.books = s.bookSeries
.map((bs) => {
const libraryItem = bs.book.libraryItem?.toJSON() const libraryItem = bs.book.libraryItem?.toJSON()
if (!libraryItem) { if (!libraryItem) {
Logger.warn(`Book series book has no libraryItem`, bs, bs.book, 'series=', series) Logger.warn(`Book series book has no libraryItem`, bs, bs.book, 'series=', series)
@ -287,7 +302,8 @@ module.exports = {
libraryItem.media = bs.book libraryItem.media = bs.book
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONMinified()
return oldLibraryItem return oldLibraryItem
}).filter(b => b) })
.filter((b) => b)
allOldSeries.push(oldSeries) allOldSeries.push(oldSeries)
} }
@ -314,7 +330,7 @@ module.exports = {
where: { where: {
libraryId: library.id, libraryId: library.id,
createdAt: { 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, replacements,
@ -329,9 +345,7 @@ module.exports = {
}, },
limit, limit,
distinct: true, distinct: true,
order: [ order: [['createdAt', 'DESC']]
['createdAt', 'DESC']
]
}) })
return { return {
@ -356,11 +370,14 @@ module.exports = {
const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit) const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit)
return { return {
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
if (li.rssFeed) { if (li.rssFeed) {
oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified() oldLibraryItem.rssFeed = Database.feedModel.getOldFeed(li.rssFeed).toJSONMinified()
} }
if (li.mediaItemShare) {
oldLibraryItem.mediaItemShare = li.mediaItemShare
}
return oldLibraryItem return oldLibraryItem
}), }),
count count
@ -380,7 +397,7 @@ module.exports = {
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'recent', null, 'createdAt', true, limit, 0) const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'recent', null, 'createdAt', true, limit, 0)
return { return {
count, count,
libraryItems: libraryItems.map(li => { libraryItems: libraryItems.map((li) => {
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified() const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(li).toJSONMinified()
oldLibraryItem.recentEpisode = li.recentEpisode oldLibraryItem.recentEpisode = li.recentEpisode
return oldLibraryItem return oldLibraryItem
@ -507,10 +524,10 @@ module.exports = {
authors.forEach((a) => data.authors.push({ id: a.id, name: a.name })) 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.genres = naturalSort([...data.genres]).asc()
data.tags = naturalSort([...data.tags]).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.narrators = naturalSort([...data.narrators]).asc()
data.publishers = naturalSort([...data.publishers]).asc() data.publishers = naturalSort([...data.publishers]).asc()
data.languages = naturalSort([...data.languages]).asc() data.languages = naturalSort([...data.languages]).asc()

View File

@ -4,6 +4,8 @@ const Logger = require('../../Logger')
const authorFilters = require('./authorFilters') const authorFilters = require('./authorFilters')
const { asciiOnlyToLowerCase } = require('../index') const { asciiOnlyToLowerCase } = require('../index')
const ShareManager = require('../../managers/ShareManager')
module.exports = { module.exports = {
/** /**
* User permissions to restrict books for explicit content & tags * User permissions to restrict books for explicit content & tags
@ -354,6 +356,7 @@ module.exports = {
sortBy = 'media.metadata.title' sortBy = 'media.metadata.title'
} }
const includeRSSFeed = include.includes('rssfeed') const includeRSSFeed = include.includes('rssfeed')
const includeMediaItemShare = include.includes('share')
// For sorting by author name an additional attribute must be added // For sorting by author name an additional attribute must be added
// with author names concatenated // with author names concatenated
@ -605,6 +608,10 @@ module.exports = {
libraryItem.rssFeed = libraryItem.feeds[0] libraryItem.rssFeed = libraryItem.feeds[0]
} }
if (includeMediaItemShare) {
libraryItem.mediaItemShare = ShareManager.findByMediaItemId(libraryItem.mediaId)
}
libraryItem.media = book libraryItem.media = book
return libraryItem return libraryItem