Add minified listening stats mode

Before stats on the app were taking about 3422ms and returning roughly 360815 bytes for /api/me/listening-stats. After adding an opt-in minified mode that skips items and recentSessions, the same dataset benchmarks at about 102ms average and 11667 bytes while keeping the default response shape unchanged for existing clients.
This commit is contained in:
Kevin Gatera 2026-03-08 18:28:01 -04:00
parent fbfa6a54c1
commit 377fb71b9c
No known key found for this signature in database
GPG key ID: F0D9F5932458CFB9
3 changed files with 40 additions and 25 deletions

View file

@ -232,10 +232,14 @@ class MeController {
*/ */
async getListeningStats(req, res) { async getListeningStats(req, res) {
const startedAt = Date.now() const startedAt = Date.now()
const minified = req.query.minified === '1' || req.query.minified === 'true'
Logger.debug(`[MeController] /api/me/listening-stats user="${req.user.id}" start`) Logger.debug(`[MeController] /api/me/listening-stats user="${req.user.id}" start`)
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id) const listeningStats = await this.getUserListeningStatsHelpers(req.user.id, {
includeItems: !minified,
includeRecentSessions: !minified
})
Logger.debug( Logger.debug(
`[MeController] /api/me/listening-stats user="${req.user.id}" totalTime=${listeningStats?.totalTime || 0} recentSessions=${listeningStats?.recentSessions?.length || 0} items=${Object.keys(listeningStats?.items || {}).length} in ${Date.now() - startedAt}ms` `[MeController] /api/me/listening-stats user="${req.user.id}" minified=${minified} totalTime=${listeningStats?.totalTime || 0} recentSessions=${listeningStats?.recentSessions?.length || 0} items=${Object.keys(listeningStats?.items || {}).length} in ${Date.now() - startedAt}ms`
) )
res.json(listeningStats) res.json(listeningStats)
} }

View file

@ -472,12 +472,16 @@ class UserController {
*/ */
async getListeningStats(req, res) { async getListeningStats(req, res) {
const startedAt = Date.now() const startedAt = Date.now()
const minified = req.query.minified === '1' || req.query.minified === 'true'
Logger.debug( Logger.debug(
`[UserController] /api/users/${req.params.id}/listening-stats reqUser="${req.user.id}" start` `[UserController] /api/users/${req.params.id}/listening-stats reqUser="${req.user.id}" start`
) )
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id) var listeningStats = await this.getUserListeningStatsHelpers(req.params.id, {
includeItems: !minified,
includeRecentSessions: !minified
})
Logger.debug( Logger.debug(
`[UserController] /api/users/${req.params.id}/listening-stats reqUser="${req.user.id}" totalTime=${listeningStats?.totalTime || 0} recentSessions=${listeningStats?.recentSessions?.length || 0} items=${Object.keys(listeningStats?.items || {}).length} in ${Date.now() - startedAt}ms` `[UserController] /api/users/${req.params.id}/listening-stats reqUser="${req.user.id}" minified=${minified} totalTime=${listeningStats?.totalTime || 0} recentSessions=${listeningStats?.recentSessions?.length || 0} items=${Object.keys(listeningStats?.items || {}).length} in ${Date.now() - startedAt}ms`
) )
res.json(listeningStats) res.json(listeningStats)
} }

View file

@ -555,27 +555,32 @@ class ApiRouter {
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
} }
async getUserListeningStatsHelpers(userId) { async getUserListeningStatsHelpers(userId, options = {}) {
const startedAt = Date.now() const startedAt = Date.now()
const today = date.format(new Date(), 'YYYY-MM-DD') const today = date.format(new Date(), 'YYYY-MM-DD')
const includeItems = options.includeItems !== false
const includeRecentSessions = options.includeRecentSessions !== false
const [listeningSessions, recentSessions] = await Promise.all([ const tasks = [Database.getPlaybackSessionsForStats({ userId })]
Database.getPlaybackSessionsForStats({ userId }), if (includeRecentSessions) {
tasks.push(
Database.getPlaybackSessions({ userId }, { Database.getPlaybackSessions({ userId }, {
limit: 10, limit: 10,
offset: 0, offset: 0,
order: [['updatedAt', 'DESC']] order: [['updatedAt', 'DESC']]
}) })
]) )
}
const [listeningSessions, recentSessions = []] = await Promise.all(tasks)
const listeningStats = { const listeningStats = {
totalTime: 0, totalTime: 0,
items: {},
days: {}, days: {},
dayOfWeek: {}, dayOfWeek: {},
today: 0, today: 0
recentSessions
} }
if (includeItems) listeningStats.items = {}
if (includeRecentSessions) listeningStats.recentSessions = recentSessions
listeningSessions.forEach((s) => { listeningSessions.forEach((s) => {
const libraryItemId = s.extraData?.libraryItemId || null const libraryItemId = s.extraData?.libraryItemId || null
let sessionTimeListening = s.timeListening let sessionTimeListening = s.timeListening
@ -600,6 +605,7 @@ class ApiRouter {
return return
} }
if (includeItems) {
if (!listeningStats.items[libraryItemId]) { if (!listeningStats.items[libraryItemId]) {
listeningStats.items[libraryItemId] = { listeningStats.items[libraryItemId] = {
id: libraryItemId, id: libraryItemId,
@ -610,11 +616,12 @@ class ApiRouter {
} else { } else {
listeningStats.items[libraryItemId].timeListening += sessionTimeListening listeningStats.items[libraryItemId].timeListening += sessionTimeListening
} }
}
listeningStats.totalTime += sessionTimeListening listeningStats.totalTime += sessionTimeListening
}) })
Logger.debug( Logger.debug(
`[ApiRouter] Listening stats for user "${userId}" aggregated ${listeningSessions.length} sessions in ${Date.now() - startedAt}ms` `[ApiRouter] Listening stats for user "${userId}" aggregated ${listeningSessions.length} sessions in ${Date.now() - startedAt}ms includeItems=${includeItems} includeRecentSessions=${includeRecentSessions}`
) )
return listeningStats return listeningStats
} }