Create new StatsController and move year in review stats endpoint

This commit is contained in:
advplyr 2025-03-29 17:34:17 -05:00
parent f853cff920
commit 4fb5330308
3 changed files with 128 additions and 3 deletions

View 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-root user "${req.user.username}" attempted to access stats route`)
return res.sendStatus(403)
}
next()
}
}
module.exports = new StatsController()

View file

@ -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.getAdminStatsForYear.bind(this))
this.router.get('/stats/server', 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))
}

View file

@ -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
}
}
}