mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-04 01:17:19 +02: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