From fc8c5fa233d3f5a02c8d60506582c2409de6b881 Mon Sep 17 00:00:00 2001 From: Zlendy Date: Sat, 19 Apr 2025 19:56:28 +0200 Subject: [PATCH 1/3] feat: API endpoint `/stats/getViewsByLibraryType` --- backend/routes/stats.js | 29 +++++++++++++++++++++++++ backend/swagger.json | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/backend/routes/stats.js b/backend/routes/stats.js index 779ced6..229b657 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -516,6 +516,35 @@ router.get("/getViewsByHour", async (req, res) => { } }); +router.get("/getViewsByLibraryType", async (req, res) => { + try { + const { days = 30 } = req.query; + + const { rows } = await db.query(` + SELECT COALESCE(i."Type", 'Other') AS type, COUNT(a."NowPlayingItemId") AS count + FROM jf_playback_activity a LEFT JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId" + WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST($1 || ' days' as INTERVAL) AND NOW() + GROUP BY i."Type" + `, [days]); + + const supportedTypes = new Set(["Audio", "Movie", "Series", "Other"]); + const reorganizedData = {}; + + rows.forEach((item) => { + const { type, count } = item; + + if (!supportedTypes.has(type)) return; + reorganizedData[type] = count; + }); + + res.send(reorganizedData); + } catch (error) { + console.log(error); + res.status(503); + res.send(error); + } +}); + router.get("/getGenreUserStats", async (req, res) => { try { const { size = 50, page = 1, userid } = req.query; diff --git a/backend/swagger.json b/backend/swagger.json index 4fc931e..00fc18b 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -3644,6 +3644,53 @@ } } }, + "/stats/getViewsByLibraryType": { + "get": { + "tags": [ + "Stats" + ], + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "x-api-token", + "in": "header", + "type": "string" + }, + { + "name": "req", + "in": "query", + "type": "string" + }, + { + "name": "days", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + }, + "503": { + "description": "Service Unavailable" + } + } + } + }, "/stats/getGenreUserStats": { "get": { "tags": [ From 8532f1ed2f0282b4c0a83eb0adec86a80e8833a2 Mon Sep 17 00:00:00 2001 From: Zlendy Date: Sat, 19 Apr 2025 20:19:45 +0200 Subject: [PATCH 2/3] fix: `/stats/getViewsByLibraryType` can return an empty object --- backend/routes/stats.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/routes/stats.js b/backend/routes/stats.js index 229b657..c75b8b4 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -537,6 +537,11 @@ router.get("/getViewsByLibraryType", async (req, res) => { reorganizedData[type] = count; }); + supportedTypes.forEach((type) => { + if (Object.prototype.hasOwnProperty.call(reorganizedData, type)) return; + reorganizedData[type] = 0; + }); + res.send(reorganizedData); } catch (error) { console.log(error); From 2fcbf78ddf62e683f4a24aeca02951af3f6c9bfb Mon Sep 17 00:00:00 2001 From: Zlendy Date: Sat, 19 Apr 2025 20:24:08 +0200 Subject: [PATCH 3/3] refactor: Use `Map` instead of `Object` --- backend/routes/stats.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/routes/stats.js b/backend/routes/stats.js index c75b8b4..a727991 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -528,21 +528,22 @@ router.get("/getViewsByLibraryType", async (req, res) => { `, [days]); const supportedTypes = new Set(["Audio", "Movie", "Series", "Other"]); - const reorganizedData = {}; + /** @type {Map} */ + const reorganizedData = new Map(); rows.forEach((item) => { const { type, count } = item; if (!supportedTypes.has(type)) return; - reorganizedData[type] = count; + reorganizedData.set(type, count); }); supportedTypes.forEach((type) => { - if (Object.prototype.hasOwnProperty.call(reorganizedData, type)) return; - reorganizedData[type] = 0; + if (reorganizedData.has(type)) return; + reorganizedData.set(type, 0); }); - res.send(reorganizedData); + res.send(Object.fromEntries(reorganizedData)); } catch (error) { console.log(error); res.status(503);