mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-26 16:48:53 +01:00
Update queries to account for user permissions
This commit is contained in:
parent
83d0db0607
commit
1372c24535
@ -207,7 +207,7 @@ class LibraryController {
|
||||
}
|
||||
payload.offset = payload.page * payload.limit
|
||||
|
||||
const { libraryItems, count } = await Database.models.libraryItem.getByFilterAndSort(req.library, req.user.id, payload)
|
||||
const { libraryItems, count } = await Database.models.libraryItem.getByFilterAndSort(req.library, req.user, payload)
|
||||
payload.results = libraryItems
|
||||
payload.total = count
|
||||
|
||||
@ -640,7 +640,7 @@ class LibraryController {
|
||||
async getUserPersonalizedShelves(req, res) {
|
||||
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 shelves = await Database.models.libraryItem.getPersonalizedShelves(req.library, req.user.id, include, limitPerShelf)
|
||||
const shelves = await Database.models.libraryItem.getPersonalizedShelves(req.library, req.user, include, limitPerShelf)
|
||||
res.json(shelves)
|
||||
}
|
||||
|
||||
|
@ -403,12 +403,12 @@ module.exports = (sequelize) => {
|
||||
/**
|
||||
* Get library items using filter and sort
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {object} options
|
||||
* @returns {object} { libraryItems:oldLibraryItem[], count:number }
|
||||
*/
|
||||
static async getByFilterAndSort(library, userId, options) {
|
||||
const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library, userId, options)
|
||||
static async getByFilterAndSort(library, user, options) {
|
||||
const { libraryItems, count } = await libraryFilters.getFilteredLibraryItems(library, user, options)
|
||||
return {
|
||||
libraryItems: libraryItems.map(li => {
|
||||
const oldLibraryItem = this.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -440,18 +440,18 @@ module.exports = (sequelize) => {
|
||||
/**
|
||||
* Get home page data personalized shelves
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object[]} array of shelf objects
|
||||
*/
|
||||
static async getPersonalizedShelves(library, userId, include, limit) {
|
||||
static async getPersonalizedShelves(library, user, include, limit) {
|
||||
const fullStart = Date.now() // Used for testing load times
|
||||
|
||||
const shelves = []
|
||||
|
||||
// "Continue Listening" shelf
|
||||
const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, userId, include, limit, false)
|
||||
const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false)
|
||||
if (itemsInProgressPayload.items.length) {
|
||||
shelves.push({
|
||||
id: 'continue-listening',
|
||||
@ -467,7 +467,7 @@ module.exports = (sequelize) => {
|
||||
let start = Date.now()
|
||||
if (library.isBook) {
|
||||
// "Continue Reading" shelf
|
||||
const ebooksInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, userId, include, limit, true)
|
||||
const ebooksInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, true)
|
||||
if (ebooksInProgressPayload.items.length) {
|
||||
shelves.push({
|
||||
id: 'continue-reading',
|
||||
@ -482,7 +482,7 @@ module.exports = (sequelize) => {
|
||||
|
||||
start = Date.now()
|
||||
// "Continue Series" shelf
|
||||
const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, userId, include, limit)
|
||||
const continueSeriesPayload = await libraryFilters.getLibraryItemsContinueSeries(library, user, include, limit)
|
||||
if (continueSeriesPayload.libraryItems.length) {
|
||||
shelves.push({
|
||||
id: 'continue-series',
|
||||
@ -496,7 +496,7 @@ module.exports = (sequelize) => {
|
||||
Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||
} else if (library.isPodcast) {
|
||||
// "Newest Episodes" shelf
|
||||
const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, userId, limit)
|
||||
const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit)
|
||||
if (newestEpisodesPayload.libraryItems.length) {
|
||||
shelves.push({
|
||||
id: 'newest-episodes',
|
||||
@ -512,7 +512,7 @@ module.exports = (sequelize) => {
|
||||
|
||||
start = Date.now()
|
||||
// "Recently Added" shelf
|
||||
const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, userId, include, limit)
|
||||
const mostRecentPayload = await libraryFilters.getLibraryItemsMostRecentlyAdded(library, user, include, limit)
|
||||
if (mostRecentPayload.libraryItems.length) {
|
||||
shelves.push({
|
||||
id: 'recently-added',
|
||||
@ -528,7 +528,7 @@ module.exports = (sequelize) => {
|
||||
if (library.isBook) {
|
||||
start = Date.now()
|
||||
// "Recent Series" shelf
|
||||
const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, include, 5)
|
||||
const seriesMostRecentPayload = await libraryFilters.getSeriesMostRecentlyAdded(library, user, include, 5)
|
||||
if (seriesMostRecentPayload.series.length) {
|
||||
shelves.push({
|
||||
id: 'recent-series',
|
||||
@ -543,7 +543,7 @@ module.exports = (sequelize) => {
|
||||
|
||||
start = Date.now()
|
||||
// "Discover" shelf
|
||||
const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, userId, include, limit)
|
||||
const discoverLibraryItemsPayload = await libraryFilters.getLibraryItemsToDiscover(library, user, include, limit)
|
||||
if (discoverLibraryItemsPayload.libraryItems.length) {
|
||||
shelves.push({
|
||||
id: 'discover',
|
||||
@ -559,7 +559,7 @@ module.exports = (sequelize) => {
|
||||
|
||||
start = Date.now()
|
||||
// "Listen Again" shelf
|
||||
const listenAgainPayload = await libraryFilters.getMediaFinished(library, userId, include, limit, false)
|
||||
const listenAgainPayload = await libraryFilters.getMediaFinished(library, user, include, limit, false)
|
||||
if (listenAgainPayload.items.length) {
|
||||
shelves.push({
|
||||
id: 'listen-again',
|
||||
@ -575,7 +575,7 @@ module.exports = (sequelize) => {
|
||||
if (library.isBook) {
|
||||
start = Date.now()
|
||||
// "Read Again" shelf
|
||||
const readAgainPayload = await libraryFilters.getMediaFinished(library, userId, include, limit, true)
|
||||
const readAgainPayload = await libraryFilters.getMediaFinished(library, user, include, limit, true)
|
||||
if (readAgainPayload.items.length) {
|
||||
shelves.push({
|
||||
id: 'read-again',
|
||||
@ -590,7 +590,7 @@ module.exports = (sequelize) => {
|
||||
|
||||
start = Date.now()
|
||||
// "Newest Authors" shelf
|
||||
const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, limit)
|
||||
const newestAuthorsPayload = await libraryFilters.getNewestAuthors(library, user, limit)
|
||||
if (newestAuthorsPayload.authors.length) {
|
||||
shelves.push({
|
||||
id: 'newest-authors',
|
||||
|
@ -12,11 +12,11 @@ module.exports = {
|
||||
/**
|
||||
* Get library items using filter and sort
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {object} options
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getFilteredLibraryItems(library, userId, options) {
|
||||
async getFilteredLibraryItems(library, user, options) {
|
||||
const { filterBy, sortBy, sortDesc, limit, offset, collapseseries, include, mediaType } = options
|
||||
|
||||
let filterValue = null
|
||||
@ -29,25 +29,25 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (mediaType === 'book') {
|
||||
return libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset)
|
||||
return libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset)
|
||||
} else {
|
||||
return libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, userId, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
|
||||
return libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get library items for continue listening & continue reading shelves
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {boolean} ebook true if continue reading shelf
|
||||
* @returns {object} { items:LibraryItem[], count:number }
|
||||
*/
|
||||
async getMediaItemsInProgress(library, userId, include, limit, ebook = false) {
|
||||
async getMediaItemsInProgress(library, user, include, limit, ebook = false) {
|
||||
if (library.mediaType === 'book') {
|
||||
const filterValue = ebook ? 'ebook-in-progress' : 'audio-in-progress'
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, 'progress', filterValue, 'progress', true, false, include, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', filterValue, 'progress', true, false, include, limit, 0)
|
||||
return {
|
||||
items: libraryItems.map(li => {
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -59,7 +59,7 @@ module.exports = {
|
||||
count
|
||||
}
|
||||
} else {
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, userId, 'progress', 'in-progress', 'progress', true, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'in-progress', 'progress', true, limit, 0)
|
||||
return {
|
||||
count,
|
||||
items: libraryItems.map(li => {
|
||||
@ -74,14 +74,14 @@ module.exports = {
|
||||
/**
|
||||
* Get library items for most recently added shelf
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getLibraryItemsMostRecentlyAdded(library, userId, include, limit) {
|
||||
async getLibraryItemsMostRecentlyAdded(library, user, include, limit) {
|
||||
if (library.mediaType === 'book') {
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, null, null, 'addedAt', true, false, include, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, null, null, 'addedAt', true, false, include, limit, 0)
|
||||
return {
|
||||
libraryItems: libraryItems.map(li => {
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -96,7 +96,7 @@ module.exports = {
|
||||
count
|
||||
}
|
||||
} else {
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, userId, null, null, 'addedAt', true, include, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredLibraryItems(library.id, user, null, null, 'addedAt', true, include, limit, 0)
|
||||
return {
|
||||
libraryItems: libraryItems.map(li => {
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -116,13 +116,13 @@ module.exports = {
|
||||
/**
|
||||
* Get library items for continue series shelf
|
||||
* @param {string} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getLibraryItemsContinueSeries(library, userId, include, limit) {
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, userId, include, limit, 0)
|
||||
async getLibraryItemsContinueSeries(library, user, include, limit) {
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getContinueSeriesLibraryItems(library.id, user, include, limit, 0)
|
||||
return {
|
||||
libraryItems: libraryItems.map(li => {
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -141,18 +141,18 @@ module.exports = {
|
||||
/**
|
||||
* Get library items or podcast episodes for the "Listen Again" or "Read Again" shelf
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {boolean} ebook true if "Read Again" shelf
|
||||
* @returns {object} { items:object[], count:number }
|
||||
*/
|
||||
async getMediaFinished(library, userId, include, limit, ebook = false) {
|
||||
async getMediaFinished(library, user, include, limit, ebook = false) {
|
||||
if (ebook && library.mediaType !== 'book') return { items: [], count: 0 }
|
||||
|
||||
if (library.mediaType === 'book') {
|
||||
const filterValue = ebook ? 'ebook-finished' : 'finished'
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, userId, 'progress', filterValue, 'progress', true, false, include, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getFilteredLibraryItems(library.id, user, 'progress', filterValue, 'progress', true, false, include, limit, 0)
|
||||
return {
|
||||
items: libraryItems.map(li => {
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -164,7 +164,7 @@ module.exports = {
|
||||
count
|
||||
}
|
||||
} else {
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, userId, 'progress', 'finished', 'progress', true, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, 'progress', 'finished', 'progress', true, limit, 0)
|
||||
return {
|
||||
count,
|
||||
items: libraryItems.map(li => {
|
||||
@ -179,11 +179,12 @@ module.exports = {
|
||||
/**
|
||||
* Get series for recent series shelf
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} { series:oldSeries[], count:number}
|
||||
*/
|
||||
async getSeriesMostRecentlyAdded(library, include, limit) {
|
||||
async getSeriesMostRecentlyAdded(library, user, include, limit) {
|
||||
if (library.mediaType !== 'book') return { series: [], count: 0 }
|
||||
|
||||
const seriesIncludes = []
|
||||
@ -192,19 +193,46 @@ module.exports = {
|
||||
model: Database.models.feed
|
||||
})
|
||||
}
|
||||
const { rows: series, count } = await Database.models.series.findAndCountAll({
|
||||
where: {
|
||||
|
||||
const userPermissionBookWhere = libraryItemsBookFilters.getUserPermissionBookWhereQuery(user)
|
||||
|
||||
const seriesWhere = [
|
||||
{
|
||||
libraryId: library.id
|
||||
},
|
||||
}
|
||||
]
|
||||
// Handle user permissions to only include series with at least 1 book
|
||||
// TODO: Simplify to a single query
|
||||
if (userPermissionBookWhere.bookWhere.length) {
|
||||
let attrQuery = 'SELECT count(*) FROM books b, bookSeries bs WHERE bs.seriesId = series.id AND bs.bookId = b.id'
|
||||
if (!user.canAccessExplicitContent) {
|
||||
attrQuery += ' AND b.explicit = 0'
|
||||
}
|
||||
if (!user.permissions.accessAllTags && user.itemTagsSelected.length) {
|
||||
if (user.permissions.selectedTagsNotAccessible) {
|
||||
attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) = 0'
|
||||
} else {
|
||||
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
|
||||
}))
|
||||
}
|
||||
|
||||
const { rows: series, count } = await Database.models.series.findAndCountAll({
|
||||
where: seriesWhere,
|
||||
limit,
|
||||
offset: 0,
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
replacements: userPermissionBookWhere.replacements,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.bookSeries,
|
||||
include: {
|
||||
model: Database.models.book,
|
||||
where: userPermissionBookWhere.bookWhere,
|
||||
include: {
|
||||
model: Database.models.libraryItem
|
||||
}
|
||||
@ -255,10 +283,11 @@ module.exports = {
|
||||
* Get most recently created authors for "Newest Authors" shelf
|
||||
* Author must be linked to at least 1 book
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {number} limit
|
||||
* @returns {object} { authors:oldAuthor[], count:number }
|
||||
*/
|
||||
async getNewestAuthors(library, limit) {
|
||||
async getNewestAuthors(library, user, limit) {
|
||||
if (library.mediaType !== 'book') return { authors: [], count: 0 }
|
||||
|
||||
const { rows: authors, count } = await Database.models.author.findAndCountAll({
|
||||
@ -288,15 +317,15 @@ module.exports = {
|
||||
/**
|
||||
* Get book library items for the "Discover" shelf
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} {libraryItems:oldLibraryItem[], count:number}
|
||||
*/
|
||||
async getLibraryItemsToDiscover(library, userId, include, limit) {
|
||||
async getLibraryItemsToDiscover(library, user, include, limit) {
|
||||
if (library.mediaType !== 'book') return { libraryItems: [], count: 0 }
|
||||
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, userId, include, limit)
|
||||
const { libraryItems, count } = await libraryItemsBookFilters.getDiscoverLibraryItems(library.id, user, include, limit)
|
||||
return {
|
||||
libraryItems: libraryItems.map(li => {
|
||||
const oldLibraryItem = Database.models.libraryItem.getOldLibraryItem(li).toJSONMinified()
|
||||
@ -312,14 +341,14 @@ module.exports = {
|
||||
/**
|
||||
* Get podcast episodes most recently added
|
||||
* @param {oldLibrary} library
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {number} limit
|
||||
* @returns {object} {libraryItems:oldLibraryItem[], count:number}
|
||||
*/
|
||||
async getNewestPodcastEpisodes(library, userId, limit) {
|
||||
async getNewestPodcastEpisodes(library, user, limit) {
|
||||
if (library.mediaType !== 'podcast') return { libraryItems: [], count: 0 }
|
||||
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, userId, null, null, 'createdAt', true, limit, 0)
|
||||
const { libraryItems, count } = await libraryItemsPodcastFilters.getFilteredPodcastEpisodes(library.id, user, null, null, 'createdAt', true, limit, 0)
|
||||
return {
|
||||
count,
|
||||
libraryItems: libraryItems.map(li => {
|
||||
|
@ -3,6 +3,35 @@ const Database = require('../../Database')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* User permissions to restrict books for explicit content & tags
|
||||
* @param {oldUser} user
|
||||
* @returns {object} { bookWhere:Sequelize.WhereOptions, replacements:string[] }
|
||||
*/
|
||||
getUserPermissionBookWhereQuery(user) {
|
||||
const bookWhere = []
|
||||
const replacements = {}
|
||||
if (!user.canAccessExplicitContent) {
|
||||
bookWhere.push({
|
||||
explicit: false
|
||||
})
|
||||
}
|
||||
if (!user.permissions.accessAllTags && user.itemTagsSelected.length) {
|
||||
replacements['userTagsSelected'] = user.itemTagsSelected
|
||||
if (user.permissions.selectedTagsNotAccessible) {
|
||||
bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0))
|
||||
} else {
|
||||
bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
}))
|
||||
}
|
||||
}
|
||||
return {
|
||||
bookWhere,
|
||||
replacements
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When collapsing series and filtering by progress
|
||||
* different where options are required
|
||||
@ -296,6 +325,7 @@ module.exports = {
|
||||
/**
|
||||
* Get library items for book media type using filter and sort
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
@ -306,7 +336,7 @@ module.exports = {
|
||||
* @param {number} offset
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset) {
|
||||
async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, collapseseries, include, limit, offset) {
|
||||
// TODO: Handle collapse sub-series
|
||||
if (filterGroup === 'series' && collapseseries) {
|
||||
collapseseries = false
|
||||
@ -442,14 +472,22 @@ module.exports = {
|
||||
model: Database.models.mediaProgress,
|
||||
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'],
|
||||
where: {
|
||||
userId
|
||||
userId: user.id
|
||||
},
|
||||
required: false
|
||||
})
|
||||
}
|
||||
|
||||
const { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||
|
||||
let bookWhere = Array.isArray(mediaWhere) ? mediaWhere : [mediaWhere]
|
||||
|
||||
// User permissions
|
||||
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
|
||||
replacements = { ...replacements, ...userPermissionBookWhere.replacements }
|
||||
bookWhere.push(...userPermissionBookWhere.bookWhere)
|
||||
|
||||
// Handle collapsed series
|
||||
let collapseSeriesBookSeries = []
|
||||
if (collapseseries) {
|
||||
let seriesBookWhere = null
|
||||
@ -461,7 +499,7 @@ module.exports = {
|
||||
['$books.authors.id$']: null
|
||||
}
|
||||
} else {
|
||||
seriesBookWhere = mediaWhere
|
||||
seriesBookWhere = bookWhere
|
||||
}
|
||||
|
||||
const bookFindOptions = {
|
||||
@ -479,9 +517,11 @@ module.exports = {
|
||||
}
|
||||
const { booksToExclude, bookSeriesToInclude } = await this.getCollapseSeriesBooksToExclude(bookFindOptions, seriesWhere)
|
||||
if (booksToExclude.length) {
|
||||
mediaWhere['id'] = {
|
||||
[Sequelize.Op.notIn]: booksToExclude
|
||||
}
|
||||
bookWhere.push({
|
||||
id: {
|
||||
[Sequelize.Op.notIn]: booksToExclude
|
||||
}
|
||||
})
|
||||
}
|
||||
collapseSeriesBookSeries = bookSeriesToInclude
|
||||
if (!bookAttributes?.include) bookAttributes = { include: [] }
|
||||
@ -496,7 +536,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
const { rows: books, count } = await Database.models.book.findAndCountAll({
|
||||
where: mediaWhere,
|
||||
where: bookWhere,
|
||||
distinct: true,
|
||||
attributes: bookAttributes,
|
||||
replacements,
|
||||
@ -572,51 +612,13 @@ module.exports = {
|
||||
* 3. Has at least 1 unfinished book
|
||||
* TODO: Reduce queries
|
||||
* @param {string} libraryId
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getContinueSeriesLibraryItems(libraryId, userId, include, limit, offset) {
|
||||
// Step 1: Get all media progress for user that belongs to a series book
|
||||
const mediaProgressForUserForSeries = await Database.models.mediaProgress.findAll({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Database.models.book,
|
||||
attributes: ['id', 'title'],
|
||||
include: {
|
||||
model: Database.models.series,
|
||||
attributes: ['id'],
|
||||
through: {
|
||||
attributes: []
|
||||
},
|
||||
required: true
|
||||
},
|
||||
required: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Step 1.5: Identify the series that have at least 1 finished book and have no books in progress
|
||||
let seriesToInclude = []
|
||||
let seriesToExclude = []
|
||||
for (const prog of mediaProgressForUserForSeries) {
|
||||
const series = prog.mediaItem?.series || []
|
||||
for (const s of series) {
|
||||
if (prog.currentTime > 0 && !prog.isFinished) { // in-progress
|
||||
seriesToInclude = seriesToInclude.filter(sid => sid !== s.id)
|
||||
if (!seriesToExclude.includes(s.id)) seriesToExclude.push(s.id)
|
||||
} else if (prog.isFinished && !seriesToExclude.includes(s.id) && !seriesToInclude.includes(s.id)) { // finished
|
||||
seriesToInclude.push(s.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// optional include rssFeed with library item
|
||||
async getContinueSeriesLibraryItems(libraryId, user, include, limit, offset) {
|
||||
const libraryItemIncludes = []
|
||||
if (include.includes('rssfeed')) {
|
||||
libraryItemIncludes.push({
|
||||
@ -624,90 +626,101 @@ module.exports = {
|
||||
})
|
||||
}
|
||||
|
||||
// Step 2: Get all series identified in step 1.5 and filter out series where all books are finished
|
||||
const bookWhere = []
|
||||
// TODO: Permissions should also be applied to subqueries
|
||||
// User permissions
|
||||
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
|
||||
bookWhere.push(...userPermissionBookWhere.bookWhere)
|
||||
|
||||
const { rows: series, count } = await Database.models.series.findAndCountAll({
|
||||
where: {
|
||||
id: {
|
||||
[Sequelize.Op.in]: seriesToInclude
|
||||
where: [
|
||||
{
|
||||
libraryId
|
||||
},
|
||||
'$bookSeries.book.mediaProgresses.isFinished$': {
|
||||
[Sequelize.Op.or]: [false, null]
|
||||
// TODO: Simplify queries
|
||||
// Has at least 1 book finished
|
||||
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM mediaProgresses mp, bookSeries bs WHERE bs.seriesId = series.id AND mp.mediaItemId = bs.bookId AND mp.userId = :userId AND mp.isFinished = 1)`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
}),
|
||||
// Has at least 1 book not finished
|
||||
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId AND mp.userId = :userId WHERE bs.seriesId = series.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
}),
|
||||
// Has no books in progress
|
||||
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM mediaProgresses mp, bookSeries bs WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id AND mp.isFinished = 0 AND mp.currentTime > 0)`), 0)
|
||||
],
|
||||
attributes: {
|
||||
include: [
|
||||
[Sequelize.literal('(SELECT max(mp.updatedAt) FROM bookSeries bs, mediaProgresses mp WHERE mp.mediaItemId = bs.bookId AND mp.userId = :userId AND bs.seriesId = series.id)'), 'recent_progress']
|
||||
]
|
||||
},
|
||||
replacements: {
|
||||
userId: user.id,
|
||||
...userPermissionBookWhere.replacements
|
||||
},
|
||||
include: {
|
||||
model: Database.models.bookSeries,
|
||||
attributes: ['bookId', 'sequence'],
|
||||
separate: true,
|
||||
subQuery: false,
|
||||
order: [
|
||||
[Sequelize.literal('CAST(sequence AS INTEGER) ASC NULLS LAST')]
|
||||
],
|
||||
where: {
|
||||
'$book.mediaProgresses.isFinished$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
}
|
||||
},
|
||||
include: {
|
||||
model: Database.models.book,
|
||||
where: bookWhere,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.libraryItem,
|
||||
include: libraryItemIncludes
|
||||
},
|
||||
{
|
||||
model: Database.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: Database.models.mediaProgress,
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
distinct: true,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.bookSeries,
|
||||
include: {
|
||||
model: Database.models.book,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.libraryItem,
|
||||
where: {
|
||||
libraryId
|
||||
},
|
||||
include: libraryItemIncludes
|
||||
},
|
||||
{
|
||||
model: Database.models.bookAuthor,
|
||||
attributes: ['authorId'],
|
||||
include: {
|
||||
model: Database.models.author
|
||||
},
|
||||
separate: true
|
||||
},
|
||||
{
|
||||
model: Database.models.mediaProgress,
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
required: false
|
||||
}
|
||||
],
|
||||
required: true
|
||||
},
|
||||
required: true
|
||||
}
|
||||
],
|
||||
order: [
|
||||
// Sort by progress most recently updated
|
||||
[Database.models.bookSeries, Database.models.book, Database.models.mediaProgress, 'updatedAt', 'DESC'],
|
||||
[Sequelize.literal('recent_progress DESC')]
|
||||
],
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
limit,
|
||||
offset
|
||||
})
|
||||
|
||||
// Step 3: Map series to library items by selecting the first unfinished book in the series
|
||||
const libraryItems = series.map(s => {
|
||||
// Natural sort sequence, nulls last
|
||||
// TODO: sort in query. was unable to sort nested association with sequelize
|
||||
s.bookSeries.sort((a, b) => {
|
||||
if (!a.sequence) return 1
|
||||
if (!b.sequence) return -1
|
||||
return a.sequence.localeCompare(b.sequence, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
})
|
||||
|
||||
// Get first unfinished book to use
|
||||
const bookSeries = s.bookSeries.find(bs => !bs.book.mediaProgresses?.[0]?.isFinished)
|
||||
const libraryItem = bookSeries.book.libraryItem.toJSON()
|
||||
|
||||
if (!s.bookSeries.length) return null // this is only possible if user has restricted books in series
|
||||
const libraryItem = s.bookSeries[0].book.libraryItem.toJSON()
|
||||
const book = s.bookSeries[0].book.toJSON()
|
||||
delete book.libraryItem
|
||||
libraryItem.series = {
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
sequence: bookSeries.sequence
|
||||
sequence: s.bookSeries[0].sequence
|
||||
}
|
||||
|
||||
if (libraryItem.feeds?.length) {
|
||||
libraryItem.rssFeed = libraryItem.feeds[0]
|
||||
}
|
||||
|
||||
libraryItem.media = bookSeries.book
|
||||
libraryItem.media = book
|
||||
return libraryItem
|
||||
})
|
||||
}).filter(s => s)
|
||||
|
||||
return {
|
||||
libraryItems,
|
||||
count
|
||||
@ -719,12 +732,14 @@ module.exports = {
|
||||
* Random selection of books that are not started
|
||||
* - only includes the first book of a not-started series
|
||||
* @param {string} libraryId
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object} {libraryItems:LibraryItem, count:number}
|
||||
*/
|
||||
async getDiscoverLibraryItems(libraryId, userId, include, limit) {
|
||||
async getDiscoverLibraryItems(libraryId, user, include, limit) {
|
||||
const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user)
|
||||
|
||||
// Step 1: Get the first book of every series that hasnt been started yet
|
||||
const seriesNotStarted = await Database.models.series.findAll({
|
||||
where: [
|
||||
@ -734,7 +749,8 @@ module.exports = {
|
||||
Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = bs.bookId WHERE bs.seriesId = series.id AND mp.userId = :userId AND (mp.isFinished = 1 OR mp.currentTime > 0))`), 0)
|
||||
],
|
||||
replacements: {
|
||||
userId
|
||||
userId: user.id,
|
||||
...userPermissionBookWhere.replacements
|
||||
},
|
||||
attributes: ['id'],
|
||||
include: {
|
||||
@ -742,6 +758,10 @@ module.exports = {
|
||||
attributes: ['bookId', 'sequence'],
|
||||
separate: true,
|
||||
required: true,
|
||||
include: {
|
||||
model: Database.models.book,
|
||||
where: userPermissionBookWhere.bookWhere
|
||||
},
|
||||
order: [
|
||||
[Sequelize.literal('CAST(sequence AS INTEGER) ASC NULLS LAST')]
|
||||
],
|
||||
@ -762,22 +782,26 @@ module.exports = {
|
||||
|
||||
// Step 2: Get books not started and not in a series OR is the first book of a series not started (ordered randomly)
|
||||
const { rows: books, count } = await Database.models.book.findAndCountAll({
|
||||
where: {
|
||||
'$mediaProgresses.isFinished$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
},
|
||||
'$mediaProgresses.currentTime$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
},
|
||||
[Sequelize.Op.or]: [
|
||||
Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0),
|
||||
{
|
||||
id: {
|
||||
[Sequelize.Op.in]: booksFromSeriesToInclude
|
||||
where: [
|
||||
{
|
||||
'$mediaProgresses.isFinished$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
},
|
||||
'$mediaProgresses.currentTime$': {
|
||||
[Sequelize.Op.or]: [null, 0]
|
||||
},
|
||||
[Sequelize.Op.or]: [
|
||||
Sequelize.where(Sequelize.literal(`(SELECT COUNT(*) FROM bookSeries bs where bs.bookId = book.id)`), 0),
|
||||
{
|
||||
id: {
|
||||
[Sequelize.Op.in]: booksFromSeriesToInclude
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
...userPermissionBookWhere.bookWhere
|
||||
],
|
||||
replacements: userPermissionBookWhere.replacements,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.libraryItem,
|
||||
@ -789,7 +813,7 @@ module.exports = {
|
||||
{
|
||||
model: Database.models.mediaProgress,
|
||||
where: {
|
||||
userId
|
||||
userId: user.id
|
||||
},
|
||||
required: false
|
||||
},
|
||||
|
@ -4,6 +4,34 @@ const Database = require('../../Database')
|
||||
const Logger = require('../../Logger')
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* User permissions to restrict podcasts for explicit content & tags
|
||||
* @param {oldUser} user
|
||||
* @returns {object} { podcastWhere:Sequelize.WhereOptions, replacements:string[] }
|
||||
*/
|
||||
getUserPermissionPodcastWhereQuery(user) {
|
||||
const podcastWhere = []
|
||||
const replacements = {}
|
||||
if (!user.canAccessExplicitContent) {
|
||||
podcastWhere.push({
|
||||
explicit: false
|
||||
})
|
||||
}
|
||||
if (!user.permissions.accessAllTags && user.itemTagsSelected.length) {
|
||||
replacements['userTagsSelected'] = user.itemTagsSelected
|
||||
if (user.permissions.selectedTagsNotAccessible) {
|
||||
podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0))
|
||||
} else {
|
||||
podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), {
|
||||
[Sequelize.Op.gte]: 1
|
||||
}))
|
||||
}
|
||||
}
|
||||
return {
|
||||
podcastWhere,
|
||||
replacements
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get where options for Podcast model
|
||||
@ -64,6 +92,7 @@ module.exports = {
|
||||
/**
|
||||
* Get library items for podcast media type using filter and sort
|
||||
* @param {string} libraryId
|
||||
* @param {oldUser} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
@ -73,7 +102,7 @@ module.exports = {
|
||||
* @param {number} offset
|
||||
* @returns {object} { libraryItems:LibraryItem[], count:number }
|
||||
*/
|
||||
async getFilteredLibraryItems(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset) {
|
||||
async getFilteredLibraryItems(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, include, limit, offset) {
|
||||
const includeRSSFeed = include.includes('rssfeed')
|
||||
const includeNumEpisodesIncomplete = include.includes('numepisodesincomplete')
|
||||
|
||||
@ -103,11 +132,18 @@ module.exports = {
|
||||
podcastIncludes.push([Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = pe.id AND mp.userId = :userId WHERE pe.podcastId = podcast.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), 'numEpisodesIncomplete'])
|
||||
}
|
||||
|
||||
const { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||
replacements.userId = userId
|
||||
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
|
||||
replacements.userId = user.id
|
||||
|
||||
const podcastWhere = []
|
||||
if (Object.keys(mediaWhere).length) podcastWhere.push(mediaWhere)
|
||||
|
||||
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
|
||||
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
|
||||
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
|
||||
|
||||
const { rows: podcasts, count } = await Database.models.podcast.findAndCountAll({
|
||||
where: mediaWhere,
|
||||
where: podcastWhere,
|
||||
replacements,
|
||||
distinct: true,
|
||||
attributes: {
|
||||
@ -157,7 +193,7 @@ module.exports = {
|
||||
/**
|
||||
* Get podcast episodes filtered and sorted
|
||||
* @param {string} libraryId
|
||||
* @param {string} userId
|
||||
* @param {oldUser} user
|
||||
* @param {[string]} filterGroup
|
||||
* @param {[string]} filterValue
|
||||
* @param {string} sortBy
|
||||
@ -166,7 +202,7 @@ module.exports = {
|
||||
* @param {number} offset
|
||||
* @returns {object} {libraryItems:LibraryItem[], count:number}
|
||||
*/
|
||||
async getFilteredPodcastEpisodes(libraryId, userId, filterGroup, filterValue, sortBy, sortDesc, limit, offset) {
|
||||
async getFilteredPodcastEpisodes(libraryId, user, filterGroup, filterValue, sortBy, sortDesc, limit, offset) {
|
||||
if (sortBy === 'progress' && filterGroup !== 'progress') {
|
||||
Logger.warn('Cannot sort podcast episodes by progress without filtering by progress')
|
||||
sortBy = 'createdAt'
|
||||
@ -178,7 +214,7 @@ module.exports = {
|
||||
podcastEpisodeIncludes.push({
|
||||
model: Database.models.mediaProgress,
|
||||
where: {
|
||||
userId
|
||||
userId: user.id
|
||||
},
|
||||
attributes: ['id', 'isFinished', 'currentTime', 'updatedAt']
|
||||
})
|
||||
@ -206,11 +242,15 @@ module.exports = {
|
||||
podcastEpisodeOrder.push([Sequelize.literal('mediaProgresses.updatedAt'), sortDesc ? 'DESC' : 'ASC'])
|
||||
}
|
||||
|
||||
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
|
||||
|
||||
const { rows: podcastEpisodes, count } = await Database.models.podcastEpisode.findAndCountAll({
|
||||
where: podcastEpisodeWhere,
|
||||
replacements: userPermissionPodcastWhere.replacements,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.podcast,
|
||||
where: userPermissionPodcastWhere.podcastWhere,
|
||||
include: [
|
||||
{
|
||||
model: Database.models.libraryItem,
|
||||
|
Loading…
Reference in New Issue
Block a user