mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05: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'] = {
 | 
			
		||||
        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]
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      distinct: true,
 | 
			
		||||
      include: [
 | 
			
		||||
        {
 | 
			
		||||
          model: Database.models.bookSeries,
 | 
			
		||||
        include: {
 | 
			
		||||
          model: Database.models.book,
 | 
			
		||||
          where: bookWhere,
 | 
			
		||||
          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.author,
 | 
			
		||||
              through: {
 | 
			
		||||
                attributes: []
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              model: Database.models.mediaProgress,
 | 
			
		||||
              where: {
 | 
			
		||||
                  userId
 | 
			
		||||
                userId: user.id
 | 
			
		||||
              },
 | 
			
		||||
              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,7 +782,8 @@ 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: {
 | 
			
		||||
      where: [
 | 
			
		||||
        {
 | 
			
		||||
          '$mediaProgresses.isFinished$': {
 | 
			
		||||
            [Sequelize.Op.or]: [null, 0]
 | 
			
		||||
          },
 | 
			
		||||
@ -778,6 +799,9 @@ module.exports = {
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        ...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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user