From 86c5e4645ae0594e1b373949d9723646ccebaba5 Mon Sep 17 00:00:00 2001 From: Kevin Gatera Date: Sun, 8 Mar 2026 23:08:33 -0400 Subject: [PATCH] Fix Postgres stats key mapping Before: the optimized Postgres stats query could aggregate 2152 sessions yet still return totalTime=0, items=0, and empty day buckets because the raw rows exposed lowercase keys instead of the camelCase fields the stats helper expected.\n\nAfter: the stats helper accepts both camelCase model fields and lowercase/raw Postgres fields, restoring totals, item aggregation, and day buckets without giving up the faster raw query path. A validation query against the same account now aggregates about 6.7M seconds across 193 items and 630 active days. --- server/routers/ApiRouter.js | 48 ++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index d6117a04e..a674a2948 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -560,6 +560,27 @@ class ApiRouter { const today = date.format(new Date(), 'YYYY-MM-DD') const includeItems = options.includeItems !== false const includeRecentSessions = options.includeRecentSessions !== false + const getSessionField = (session, key) => { + if (!session || typeof session !== 'object') return undefined + if (session[key] !== undefined) return session[key] + + const lowerKey = key.toLowerCase() + if (session[lowerKey] !== undefined) return session[lowerKey] + + return undefined + } + const getSessionObjectField = (session, key) => { + const value = getSessionField(session, key) + if (!value) return null + if (typeof value === 'string') { + try { + return JSON.parse(value) + } catch (error) { + return null + } + } + return value + } const tasks = [Database.getPlaybackSessionsForStats({ userId })] if (includeRecentSessions) { @@ -582,21 +603,26 @@ class ApiRouter { if (includeItems) listeningStats.items = {} if (includeRecentSessions) listeningStats.recentSessions = recentSessions listeningSessions.forEach((s) => { - const libraryItemId = s.extraData?.libraryItemId || null - const numericListening = Number(s.timeListening) + const extraData = getSessionObjectField(s, 'extraData') + const libraryItemId = extraData?.libraryItemId || extraData?.libraryitemid || null + const sessionDate = getSessionField(s, 'date') + const sessionDayOfWeek = getSessionField(s, 'dayOfWeek') + const sessionUpdatedAt = getSessionField(s, 'updatedAt') + const sessionMediaMetadata = getSessionObjectField(s, 'mediaMetadata') + const numericListening = Number(getSessionField(s, 'timeListening')) const sessionTimeListening = Number.isFinite(numericListening) ? numericListening : 0 - if (s.dayOfWeek) { - if (!listeningStats.dayOfWeek[s.dayOfWeek]) listeningStats.dayOfWeek[s.dayOfWeek] = 0 - listeningStats.dayOfWeek[s.dayOfWeek] += sessionTimeListening + if (sessionDayOfWeek) { + if (!listeningStats.dayOfWeek[sessionDayOfWeek]) listeningStats.dayOfWeek[sessionDayOfWeek] = 0 + listeningStats.dayOfWeek[sessionDayOfWeek] += sessionTimeListening } - if (s.date && sessionTimeListening > 0) { - if (!listeningStats.days[s.date]) listeningStats.days[s.date] = 0 - listeningStats.days[s.date] += sessionTimeListening + if (sessionDate && sessionTimeListening > 0) { + if (!listeningStats.days[sessionDate]) listeningStats.days[sessionDate] = 0 + listeningStats.days[sessionDate] += sessionTimeListening - if (s.date === today) { + if (sessionDate === today) { listeningStats.today += sessionTimeListening } } @@ -610,8 +636,8 @@ class ApiRouter { listeningStats.items[libraryItemId] = { id: libraryItemId, timeListening: sessionTimeListening, - mediaMetadata: s.mediaMetadata, - lastUpdate: s.updatedAt + mediaMetadata: sessionMediaMetadata, + lastUpdate: sessionUpdatedAt } } else { listeningStats.items[libraryItemId].timeListening += sessionTimeListening