mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge pull request #4168 from advplyr/new_stats_controller
Create new StatsController and move year in review stats endpoint
This commit is contained in:
		
						commit
						8bea5d83f5
					
				
							
								
								
									
										75
									
								
								server/controllers/StatsController.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								server/controllers/StatsController.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					const { Request, Response, NextFunction } = require('express')
 | 
				
			||||||
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const adminStats = require('../utils/queries/adminStats')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @typedef RequestUserObject
 | 
				
			||||||
 | 
					 * @property {import('../models/User')} user
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @typedef {Request & RequestUserObject} RequestWithUser
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StatsController {
 | 
				
			||||||
 | 
					  constructor() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * GET: /api/stats/server
 | 
				
			||||||
 | 
					   * Currently not in use
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {RequestWithUser} req
 | 
				
			||||||
 | 
					   * @param {Response} res
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async getServerStats(req, res) {
 | 
				
			||||||
 | 
					    Logger.debug('[StatsController] getServerStats')
 | 
				
			||||||
 | 
					    const totalSize = await adminStats.getTotalSize()
 | 
				
			||||||
 | 
					    const numAudioFiles = await adminStats.getNumAudioFiles()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json({
 | 
				
			||||||
 | 
					      books: {
 | 
				
			||||||
 | 
					        ...totalSize.books,
 | 
				
			||||||
 | 
					        numAudioFiles: numAudioFiles.numBookAudioFiles
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      podcasts: {
 | 
				
			||||||
 | 
					        ...totalSize.podcasts,
 | 
				
			||||||
 | 
					        numAudioFiles: numAudioFiles.numPodcastAudioFiles
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      total: {
 | 
				
			||||||
 | 
					        ...totalSize.total,
 | 
				
			||||||
 | 
					        numAudioFiles: numAudioFiles.numAudioFiles
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * GET: /api/stats/year/:year
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {RequestWithUser} req
 | 
				
			||||||
 | 
					   * @param {Response} res
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async getAdminStatsForYear(req, res) {
 | 
				
			||||||
 | 
					    const year = Number(req.params.year)
 | 
				
			||||||
 | 
					    if (isNaN(year) || year < 2000 || year > 9999) {
 | 
				
			||||||
 | 
					      Logger.error(`[StatsController] Invalid year "${year}"`)
 | 
				
			||||||
 | 
					      return res.status(400).send('Invalid year')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const stats = await adminStats.getStatsForYear(year)
 | 
				
			||||||
 | 
					    res.json(stats)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param {RequestWithUser} req
 | 
				
			||||||
 | 
					   * @param {Response} res
 | 
				
			||||||
 | 
					   * @param {NextFunction} next
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async middleware(req, res, next) {
 | 
				
			||||||
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
 | 
					      Logger.error(`[StatsController] Non-admin user "${req.user.username}" attempted to access stats route`)
 | 
				
			||||||
 | 
					      return res.sendStatus(403)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    next()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					module.exports = new StatsController()
 | 
				
			||||||
@ -33,8 +33,7 @@ const RSSFeedController = require('../controllers/RSSFeedController')
 | 
				
			|||||||
const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController')
 | 
					const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController')
 | 
				
			||||||
const MiscController = require('../controllers/MiscController')
 | 
					const MiscController = require('../controllers/MiscController')
 | 
				
			||||||
const ShareController = require('../controllers/ShareController')
 | 
					const ShareController = require('../controllers/ShareController')
 | 
				
			||||||
 | 
					const StatsController = require('../controllers/StatsController')
 | 
				
			||||||
const { getTitleIgnorePrefix } = require('../utils/index')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ApiRouter {
 | 
					class ApiRouter {
 | 
				
			||||||
  constructor(Server) {
 | 
					  constructor(Server) {
 | 
				
			||||||
@ -320,6 +319,12 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this))
 | 
					    this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this))
 | 
				
			||||||
    this.router.delete('/share/mediaitem/:id', ShareController.deleteMediaItemShare.bind(this))
 | 
					    this.router.delete('/share/mediaitem/:id', ShareController.deleteMediaItemShare.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // Stats Routes
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    this.router.get('/stats/year/:year', StatsController.middleware.bind(this), StatsController.getAdminStatsForYear.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/stats/server', StatsController.middleware.bind(this), StatsController.getServerStats.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    // Misc Routes
 | 
					    // Misc Routes
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
@ -338,7 +343,6 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this))
 | 
					    this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this))
 | 
				
			||||||
    this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this))
 | 
					    this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this))
 | 
				
			||||||
    this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this))
 | 
					    this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this))
 | 
				
			||||||
    this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this))
 | 
					 | 
				
			||||||
    this.router.get('/logger-data', MiscController.getLoggerData.bind(this))
 | 
					    this.router.get('/logger-data', MiscController.getLoggerData.bind(this))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -167,5 +167,51 @@ module.exports = {
 | 
				
			|||||||
      topNarrators,
 | 
					      topNarrators,
 | 
				
			||||||
      topGenres
 | 
					      topGenres
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get total file size and number of items for books and podcasts
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @typedef {Object} SizeObject
 | 
				
			||||||
 | 
					   * @property {number} totalSize
 | 
				
			||||||
 | 
					   * @property {number} numItems
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {Promise<{books: SizeObject, podcasts: SizeObject, total: SizeObject}}>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async getTotalSize() {
 | 
				
			||||||
 | 
					    const [mediaTypeStats] = await Database.sequelize.query(`SELECT li.mediaType, SUM(li.size) AS totalSize, COUNT(*) AS numItems FROM libraryItems li group by li.mediaType;`)
 | 
				
			||||||
 | 
					    const bookStats = mediaTypeStats.find((m) => m.mediaType === 'book')
 | 
				
			||||||
 | 
					    const podcastStats = mediaTypeStats.find((m) => m.mediaType === 'podcast')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      books: {
 | 
				
			||||||
 | 
					        totalSize: bookStats?.totalSize || 0,
 | 
				
			||||||
 | 
					        numItems: bookStats?.numItems || 0
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      podcasts: {
 | 
				
			||||||
 | 
					        totalSize: podcastStats?.totalSize || 0,
 | 
				
			||||||
 | 
					        numItems: podcastStats?.numItems || 0
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      total: {
 | 
				
			||||||
 | 
					        totalSize: (bookStats?.totalSize || 0) + (podcastStats?.totalSize || 0),
 | 
				
			||||||
 | 
					        numItems: (bookStats?.numItems || 0) + (podcastStats?.numItems || 0)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get total number of audio files for books and podcasts
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @returns {Promise<{numBookAudioFiles: number, numPodcastAudioFiles: number, numAudioFiles: number}>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async getNumAudioFiles() {
 | 
				
			||||||
 | 
					    const [numBookAudioFilesRow] = await Database.sequelize.query(`SELECT SUM(json_array_length(b.audioFiles)) AS numAudioFiles FROM books b;`)
 | 
				
			||||||
 | 
					    const numBookAudioFiles = numBookAudioFilesRow[0]?.numAudioFiles || 0
 | 
				
			||||||
 | 
					    const numPodcastAudioFiles = await Database.podcastEpisodeModel.count()
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      numBookAudioFiles,
 | 
				
			||||||
 | 
					      numPodcastAudioFiles,
 | 
				
			||||||
 | 
					      numAudioFiles: numBookAudioFiles + numPodcastAudioFiles
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user