mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	Update library stats API route to load from db
This commit is contained in:
		
							parent
							
								
									ff0d6326d3
								
							
						
					
					
						commit
						332078e6c1
					
				@ -18,7 +18,7 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="flex px-4">
 | 
			
		||||
    <div v-if="isBookLibrary" class="flex px-4">
 | 
			
		||||
      <svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
 | 
			
		||||
        <path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
 | 
			
		||||
      </svg>
 | 
			
		||||
@ -58,26 +58,32 @@ export default {
 | 
			
		||||
    return {}
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    currentLibraryMediaType() {
 | 
			
		||||
      return this.$store.getters['libraries/getCurrentLibraryMediaType']
 | 
			
		||||
    },
 | 
			
		||||
    isBookLibrary() {
 | 
			
		||||
      return this.currentLibraryMediaType === 'book'
 | 
			
		||||
    },
 | 
			
		||||
    user() {
 | 
			
		||||
      return this.$store.state.user.user
 | 
			
		||||
    },
 | 
			
		||||
    totalItems() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.totalItems : 0
 | 
			
		||||
      return this.libraryStats?.totalItems || 0
 | 
			
		||||
    },
 | 
			
		||||
    totalAuthors() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.totalAuthors : 0
 | 
			
		||||
      return this.libraryStats?.totalAuthors || 0
 | 
			
		||||
    },
 | 
			
		||||
    numAudioTracks() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.numAudioTracks : 0
 | 
			
		||||
      return this.libraryStats?.numAudioTracks || 0
 | 
			
		||||
    },
 | 
			
		||||
    totalDuration() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.totalDuration : 0
 | 
			
		||||
      return this.libraryStats?.totalDuration || 0
 | 
			
		||||
    },
 | 
			
		||||
    totalHours() {
 | 
			
		||||
      return Math.round(this.totalDuration / (60 * 60))
 | 
			
		||||
    },
 | 
			
		||||
    totalSizePretty() {
 | 
			
		||||
      var totalSize = this.libraryStats ? this.libraryStats.totalSize : 0
 | 
			
		||||
      var totalSize = this.libraryStats?.totalSize || 0
 | 
			
		||||
      return this.$bytesPretty(totalSize, 1)
 | 
			
		||||
    },
 | 
			
		||||
    totalSizeNum() {
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="w-80 my-6 mx-auto">
 | 
			
		||||
        <div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
 | 
			
		||||
          <h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
 | 
			
		||||
          <p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
 | 
			
		||||
          <template v-for="(author, index) in top10Authors">
 | 
			
		||||
@ -114,43 +114,49 @@ export default {
 | 
			
		||||
      return this.$store.state.user.user
 | 
			
		||||
    },
 | 
			
		||||
    totalItems() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.totalItems : 0
 | 
			
		||||
      return this.libraryStats?.totalItems || 0
 | 
			
		||||
    },
 | 
			
		||||
    genresWithCount() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.genresWithCount : []
 | 
			
		||||
      return this.libraryStats?.genresWithCount || []
 | 
			
		||||
    },
 | 
			
		||||
    top5Genres() {
 | 
			
		||||
      return this.genresWithCount.slice(0, 5)
 | 
			
		||||
      return this.genresWithCount?.slice(0, 5) || []
 | 
			
		||||
    },
 | 
			
		||||
    top10LongestItems() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.longestItems || [] : []
 | 
			
		||||
      return this.libraryStats?.longestItems || []
 | 
			
		||||
    },
 | 
			
		||||
    longestItemDuration() {
 | 
			
		||||
      if (!this.top10LongestItems.length) return 0
 | 
			
		||||
      return this.top10LongestItems[0].duration
 | 
			
		||||
    },
 | 
			
		||||
    top10LargestItems() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.largestItems || [] : []
 | 
			
		||||
      return this.libraryStats?.largestItems || []
 | 
			
		||||
    },
 | 
			
		||||
    largestItemSize() {
 | 
			
		||||
      if (!this.top10LargestItems.length) return 0
 | 
			
		||||
      return this.top10LargestItems[0].size
 | 
			
		||||
    },
 | 
			
		||||
    authorsWithCount() {
 | 
			
		||||
      return this.libraryStats ? this.libraryStats.authorsWithCount : []
 | 
			
		||||
      return this.libraryStats?.authorsWithCount || []
 | 
			
		||||
    },
 | 
			
		||||
    mostUsedAuthorCount() {
 | 
			
		||||
      if (!this.authorsWithCount.length) return 0
 | 
			
		||||
      return this.authorsWithCount[0].count
 | 
			
		||||
    },
 | 
			
		||||
    top10Authors() {
 | 
			
		||||
      return this.authorsWithCount.slice(0, 10)
 | 
			
		||||
      return this.authorsWithCount?.slice(0, 10) || []
 | 
			
		||||
    },
 | 
			
		||||
    currentLibraryId() {
 | 
			
		||||
      return this.$store.state.libraries.currentLibraryId
 | 
			
		||||
    },
 | 
			
		||||
    currentLibraryName() {
 | 
			
		||||
      return this.$store.getters['libraries/getCurrentLibraryName']
 | 
			
		||||
    },
 | 
			
		||||
    currentLibraryMediaType() {
 | 
			
		||||
      return this.$store.getters['libraries/getCurrentLibraryMediaType']
 | 
			
		||||
    },
 | 
			
		||||
    isBookLibrary() {
 | 
			
		||||
      return this.currentLibraryMediaType === 'book'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ const naturalSort = createNewSortInstance({
 | 
			
		||||
const Database = require('../Database')
 | 
			
		||||
const libraryFilters = require('../utils/queries/libraryFilters')
 | 
			
		||||
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
 | 
			
		||||
const authorFilters = require('../utils/queries/authorFilters')
 | 
			
		||||
 | 
			
		||||
class LibraryController {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
@ -809,23 +810,44 @@ class LibraryController {
 | 
			
		||||
    res.json(matches)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * GET: /api/libraries/:id/stats
 | 
			
		||||
   * Get stats for library
 | 
			
		||||
   * @param {import('express').Request} req 
 | 
			
		||||
   * @param {import('express').Response} res 
 | 
			
		||||
   */
 | 
			
		||||
  async stats(req, res) {
 | 
			
		||||
    var libraryItems = req.libraryItems
 | 
			
		||||
    var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems)
 | 
			
		||||
    var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems)
 | 
			
		||||
    var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
 | 
			
		||||
    var sizeStats = libraryHelpers.getItemSizeStats(libraryItems)
 | 
			
		||||
    var stats = {
 | 
			
		||||
      totalItems: libraryItems.length,
 | 
			
		||||
      totalAuthors: Object.keys(authorsWithCount).length,
 | 
			
		||||
      totalGenres: Object.keys(genresWithCount).length,
 | 
			
		||||
      totalDuration: durationStats.totalDuration,
 | 
			
		||||
      longestItems: durationStats.longestItems,
 | 
			
		||||
      numAudioTracks: durationStats.numAudioTracks,
 | 
			
		||||
      totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems),
 | 
			
		||||
      largestItems: sizeStats.largestItems,
 | 
			
		||||
      authorsWithCount,
 | 
			
		||||
      genresWithCount
 | 
			
		||||
    const stats = {
 | 
			
		||||
      largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (req.library.isBook) {
 | 
			
		||||
      const authors = await authorFilters.getAuthorsWithCount(req.library.id)
 | 
			
		||||
      const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
 | 
			
		||||
      const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
 | 
			
		||||
      const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)
 | 
			
		||||
 | 
			
		||||
      stats.totalAuthors = authors.length
 | 
			
		||||
      stats.authorsWithCount = authors
 | 
			
		||||
      stats.totalGenres = genres.length
 | 
			
		||||
      stats.genresWithCount = genres
 | 
			
		||||
      stats.totalItems = bookStats.totalItems
 | 
			
		||||
      stats.longestItems = longestBooks
 | 
			
		||||
      stats.totalSize = bookStats.totalSize
 | 
			
		||||
      stats.totalDuration = bookStats.totalDuration
 | 
			
		||||
      stats.numAudioTracks = bookStats.numAudioFiles
 | 
			
		||||
    } else {
 | 
			
		||||
      const genres = await libraryItemsPodcastFilters.getGenresWithCount(req.library.id)
 | 
			
		||||
      const podcastStats = await libraryItemsPodcastFilters.getPodcastLibraryStats(req.library.id)
 | 
			
		||||
      const longestPodcasts = await libraryItemsPodcastFilters.getLongestPodcasts(req.library.id, 10)
 | 
			
		||||
 | 
			
		||||
      stats.totalGenres = genres.length
 | 
			
		||||
      stats.genresWithCount = genres
 | 
			
		||||
      stats.totalItems = podcastStats.totalItems
 | 
			
		||||
      stats.longestItems = longestPodcasts
 | 
			
		||||
      stats.totalSize = podcastStats.totalSize
 | 
			
		||||
      stats.totalDuration = podcastStats.totalDuration
 | 
			
		||||
      stats.numAudioTracks = podcastStats.numAudioFiles
 | 
			
		||||
    }
 | 
			
		||||
    res.json(stats)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@ class ApiRouter {
 | 
			
		||||
    this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
 | 
			
		||||
    this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this))
 | 
			
		||||
    this.router.get('/libraries/:id/search', LibraryController.middlewareNew.bind(this), LibraryController.search.bind(this))
 | 
			
		||||
    this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
 | 
			
		||||
    this.router.get('/libraries/:id/stats', LibraryController.middlewareNew.bind(this), LibraryController.stats.bind(this))
 | 
			
		||||
    this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this))
 | 
			
		||||
    this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this))
 | 
			
		||||
    this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.updateNarrator.bind(this))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										69
									
								
								server/utils/queries/authorFilters.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								server/utils/queries/authorFilters.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
			
		||||
const Sequelize = require('sequelize')
 | 
			
		||||
const Logger = require('../../Logger')
 | 
			
		||||
const Database = require('../../Database')
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  /**
 | 
			
		||||
   * Get authors with count of num books
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @returns {{id:string, name:string, count:number}}
 | 
			
		||||
   */
 | 
			
		||||
  async getAuthorsWithCount(libraryId) {
 | 
			
		||||
    const authors = await Database.authorModel.findAll({
 | 
			
		||||
      where: [
 | 
			
		||||
        {
 | 
			
		||||
          libraryId
 | 
			
		||||
        },
 | 
			
		||||
        Sequelize.where(Sequelize.literal('count'), {
 | 
			
		||||
          [Sequelize.Op.gt]: 0
 | 
			
		||||
        })
 | 
			
		||||
      ],
 | 
			
		||||
      attributes: [
 | 
			
		||||
        'id',
 | 
			
		||||
        'name',
 | 
			
		||||
        [Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'count']
 | 
			
		||||
      ],
 | 
			
		||||
      order: [
 | 
			
		||||
        ['count', 'DESC']
 | 
			
		||||
      ]
 | 
			
		||||
    })
 | 
			
		||||
    return authors.map(au => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: au.id,
 | 
			
		||||
        name: au.name,
 | 
			
		||||
        count: au.dataValues.count
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Search authors
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @param {string} query 
 | 
			
		||||
   * @returns {object[]} oldAuthor with numBooks
 | 
			
		||||
   */
 | 
			
		||||
  async search(libraryId, query) {
 | 
			
		||||
    const authors = await Database.authorModel.findAll({
 | 
			
		||||
      where: {
 | 
			
		||||
        name: {
 | 
			
		||||
          [Sequelize.Op.substring]: query
 | 
			
		||||
        },
 | 
			
		||||
        libraryId
 | 
			
		||||
      },
 | 
			
		||||
      attributes: {
 | 
			
		||||
        include: [
 | 
			
		||||
          [Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      limit,
 | 
			
		||||
      offset
 | 
			
		||||
    })
 | 
			
		||||
    const authorMatches = []
 | 
			
		||||
    for (const author of authors) {
 | 
			
		||||
      const oldAuthor = author.getOldAuthor().toJSON()
 | 
			
		||||
      oldAuthor.numBooks = author.dataValues.numBooks
 | 
			
		||||
      authorMatches.push(oldAuthor)
 | 
			
		||||
    }
 | 
			
		||||
    return authorMatches
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -180,5 +180,41 @@ module.exports = {
 | 
			
		||||
    } else {
 | 
			
		||||
      return libraryItemsPodcastFilters.search(oldUser, oldLibrary, query, limit, 0)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get largest items in library
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @param {number} limit 
 | 
			
		||||
   * @returns {Promise<{ id:string, title:string, size:number }[]>}
 | 
			
		||||
   */
 | 
			
		||||
  async getLargestItems(libraryId, limit) {
 | 
			
		||||
    const libraryItems = await Database.libraryItemModel.findAll({
 | 
			
		||||
      attributes: ['id', 'mediaId', 'mediaType', 'size'],
 | 
			
		||||
      where: {
 | 
			
		||||
        libraryId
 | 
			
		||||
      },
 | 
			
		||||
      include: [
 | 
			
		||||
        {
 | 
			
		||||
          model: Database.bookModel,
 | 
			
		||||
          attributes: ['id', 'title']
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          model: Database.podcastModel,
 | 
			
		||||
          attributes: ['id', 'title']
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      order: [
 | 
			
		||||
        ['size', 'DESC']
 | 
			
		||||
      ],
 | 
			
		||||
      limit
 | 
			
		||||
    })
 | 
			
		||||
    return libraryItems.map(libraryItem => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: libraryItem.id,
 | 
			
		||||
        title: libraryItem.media.title,
 | 
			
		||||
        size: libraryItem.size
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
const Sequelize = require('sequelize')
 | 
			
		||||
const Database = require('../../Database')
 | 
			
		||||
const Logger = require('../../Logger')
 | 
			
		||||
const authorFilters = require('./authorFilters')
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  /**
 | 
			
		||||
@ -1098,27 +1099,7 @@ module.exports = {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Search authors
 | 
			
		||||
    const authors = await Database.authorModel.findAll({
 | 
			
		||||
      where: {
 | 
			
		||||
        name: {
 | 
			
		||||
          [Sequelize.Op.substring]: query
 | 
			
		||||
        },
 | 
			
		||||
        libraryId: oldLibrary.id
 | 
			
		||||
      },
 | 
			
		||||
      attributes: {
 | 
			
		||||
        include: [
 | 
			
		||||
          [Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      limit,
 | 
			
		||||
      offset
 | 
			
		||||
    })
 | 
			
		||||
    const authorMatches = []
 | 
			
		||||
    for (const author of authors) {
 | 
			
		||||
      const oldAuthor = author.getOldAuthor().toJSON()
 | 
			
		||||
      oldAuthor.numBooks = author.dataValues.numBooks
 | 
			
		||||
      authorMatches.push(oldAuthor)
 | 
			
		||||
    }
 | 
			
		||||
    const authorMatches = await authorFilters.search(oldLibrary.id, query)
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      book: itemMatches,
 | 
			
		||||
@ -1127,5 +1108,71 @@ module.exports = {
 | 
			
		||||
      series: seriesMatches,
 | 
			
		||||
      authors: authorMatches
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Genres with num books
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @returns {{genre:string, count:number}[]}
 | 
			
		||||
   */
 | 
			
		||||
  async getGenresWithCount(libraryId) {
 | 
			
		||||
    const genres = []
 | 
			
		||||
    const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.genres) WHERE json_valid(b.genres) AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC;`, {
 | 
			
		||||
      replacements: {
 | 
			
		||||
        libraryId
 | 
			
		||||
      },
 | 
			
		||||
      raw: true
 | 
			
		||||
    })
 | 
			
		||||
    for (const row of genreResults) {
 | 
			
		||||
      genres.push({
 | 
			
		||||
        genre: row.value,
 | 
			
		||||
        count: row.numItems
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return genres
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get stats for book library
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
 | 
			
		||||
   */
 | 
			
		||||
  async getBookLibraryStats(libraryId) {
 | 
			
		||||
    const [statResults] = await Database.sequelize.query(`SELECT SUM(li.size) AS totalSize, SUM(b.duration) AS totalDuration, SUM(json_array_length(b.audioFiles)) AS numAudioFiles, COUNT(*) AS totalItems FROM libraryItems li, books b WHERE b.id = li.mediaId AND li.libraryId = :libraryId;`, {
 | 
			
		||||
      replacements: {
 | 
			
		||||
        libraryId
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return statResults[0]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get longest books in library
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @param {number} limit 
 | 
			
		||||
   * @returns {Promise<{ id:string, title:string, duration:number }[]>}
 | 
			
		||||
   */
 | 
			
		||||
  async getLongestBooks(libraryId, limit) {
 | 
			
		||||
    const books = await Database.bookModel.findAll({
 | 
			
		||||
      attributes: ['id', 'title', 'duration'],
 | 
			
		||||
      include: {
 | 
			
		||||
        model: Database.libraryItemModel,
 | 
			
		||||
        attributes: ['id', 'libraryId'],
 | 
			
		||||
        where: {
 | 
			
		||||
          libraryId
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      order: [
 | 
			
		||||
        ['duration', 'DESC']
 | 
			
		||||
      ],
 | 
			
		||||
      limit
 | 
			
		||||
    })
 | 
			
		||||
    return books.map(book => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: book.libraryItem.id,
 | 
			
		||||
        title: book.title,
 | 
			
		||||
        duration: book.duration
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -455,5 +455,75 @@ module.exports = {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    return episodeResults
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get stats for podcast library
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
 | 
			
		||||
   */
 | 
			
		||||
  async getPodcastLibraryStats(libraryId) {
 | 
			
		||||
    const [statResults] = await Database.sequelize.query(`SELECT SUM(json_extract(pe.audioFile, '$.duration')) AS totalDuration, SUM(li.size) AS totalSize, COUNT(DISTINCT(li.id)) AS totalItems, COUNT(pe.id) AS numAudioFiles FROM libraryItems li, podcasts p LEFT OUTER JOIN podcastEpisodes pe ON pe.podcastId = p.id WHERE p.id = li.mediaId AND li.libraryId = :libraryId;`, {
 | 
			
		||||
      replacements: {
 | 
			
		||||
        libraryId
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return statResults[0]
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Genres with num podcasts
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @returns {{genre:string, count:number}[]}
 | 
			
		||||
   */
 | 
			
		||||
  async getGenresWithCount(libraryId) {
 | 
			
		||||
    const genres = []
 | 
			
		||||
    const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.genres) WHERE json_valid(p.genres) AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC;`, {
 | 
			
		||||
      replacements: {
 | 
			
		||||
        libraryId
 | 
			
		||||
      },
 | 
			
		||||
      raw: true
 | 
			
		||||
    })
 | 
			
		||||
    for (const row of genreResults) {
 | 
			
		||||
      genres.push({
 | 
			
		||||
        genre: row.value,
 | 
			
		||||
        count: row.numItems
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return genres
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get longest podcasts in library
 | 
			
		||||
   * @param {string} libraryId 
 | 
			
		||||
   * @param {number} limit 
 | 
			
		||||
   * @returns {Promise<{ id:string, title:string, duration:number }[]>}
 | 
			
		||||
   */
 | 
			
		||||
  async getLongestPodcasts(libraryId, limit) {
 | 
			
		||||
    const podcasts = await Database.podcastModel.findAll({
 | 
			
		||||
      attributes: [
 | 
			
		||||
        'id',
 | 
			
		||||
        'title',
 | 
			
		||||
        [Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']
 | 
			
		||||
      ],
 | 
			
		||||
      include: {
 | 
			
		||||
        model: Database.libraryItemModel,
 | 
			
		||||
        attributes: ['id', 'libraryId'],
 | 
			
		||||
        where: {
 | 
			
		||||
          libraryId
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      order: [
 | 
			
		||||
        ['duration', 'DESC']
 | 
			
		||||
      ],
 | 
			
		||||
      limit
 | 
			
		||||
    })
 | 
			
		||||
    return podcasts.map(podcast => {
 | 
			
		||||
      return {
 | 
			
		||||
        id: podcast.libraryItem.id,
 | 
			
		||||
        title: podcast.title,
 | 
			
		||||
        duration: podcast.dataValues.duration
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user