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 MiscController = require('../controllers/MiscController') | ||||
| const ShareController = require('../controllers/ShareController') | ||||
| 
 | ||||
| const { getTitleIgnorePrefix } = require('../utils/index') | ||||
| const StatsController = require('../controllers/StatsController') | ||||
| 
 | ||||
| class ApiRouter { | ||||
|   constructor(Server) { | ||||
| @ -320,6 +319,12 @@ class ApiRouter { | ||||
|     this.router.post('/share/mediaitem', ShareController.createMediaItemShare.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
 | ||||
|     //
 | ||||
| @ -338,7 +343,6 @@ class ApiRouter { | ||||
|     this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) | ||||
|     this.router.patch('/auth-settings', MiscController.updateAuthSettings.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)) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -167,5 +167,51 @@ module.exports = { | ||||
|       topNarrators, | ||||
|       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