Update queries to account for user permissions

This commit is contained in:
advplyr 2023-08-06 13:36:58 -05:00
parent 83d0db0607
commit 1372c24535
5 changed files with 278 additions and 185 deletions

View File

@ -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)
}

View File

@ -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',

View File

@ -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 => {

View File

@ -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
},

View File

@ -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,