From a8e4c9f00bff21678c5c8c7d27e2cb777e751576 Mon Sep 17 00:00:00 2001 From: CyferShepard Date: Sun, 30 Mar 2025 17:46:07 +0200 Subject: [PATCH] generalized Genre stat card Added Genre stat card to libary views #292 bug fixes on Genre chart view --- backend/routes/stats.js | 65 +++++++++++++++- src/pages/components/library-info.jsx | 3 + .../library/genre-library-stats.jsx | 77 +++++++++++++++++++ .../genre-stat-card.jsx | 4 +- src/pages/components/user-info.jsx | 4 +- .../{genre-stats.jsx => genre-user-stats.jsx} | 8 +- 6 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 src/pages/components/library/genre-library-stats.jsx rename src/pages/components/{user-info => statCards}/genre-stat-card.jsx (94%) rename src/pages/components/user-info/{genre-stats.jsx => genre-user-stats.jsx} (90%) diff --git a/backend/routes/stats.js b/backend/routes/stats.js index 4dd5537..8611273 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -516,7 +516,7 @@ router.post("/getViewsByHour", async (req, res) => { } }); -router.get("/getGenreStats", async (req, res) => { +router.get("/getGenreUserStats", async (req, res) => { try { const { size = 50, page = 1, userid } = req.query; @@ -579,6 +579,69 @@ router.get("/getGenreStats", async (req, res) => { } }); +router.get("/getGenreLibraryStats", async (req, res) => { + try { + const { size = 50, page = 1, libraryid } = req.query; + + if (libraryid === undefined) { + res.status(400); + res.send("No Library ID provided"); + return; + } + + const values = []; + const query = { + select: ["COALESCE(g.genre, 'No Genre') AS genre", `SUM(a."PlaybackDuration") AS duration`, "COUNT(*) AS plays"], + table: "jf_playback_activity_with_metadata", + alias: "a", + joins: [ + { + type: "inner", + table: "jf_library_items", + alias: "i", + conditions: [{ first: "a.NowPlayingItemId", operator: "=", second: "i.Id" }], + }, + { + type: "left", + table: ` + LATERAL ( + SELECT + jsonb_array_elements_text( + CASE + WHEN jsonb_array_length(COALESCE(i."Genres", '[]'::jsonb)) = 0 THEN '["No Genre"]'::jsonb + ELSE i."Genres" + END + ) AS genre + ) + `, + alias: "g", + conditions: [{ first: 1, operator: "=", value: 1, wrap: false }], + }, + ], + + where: [[{ column: "a.ParentId", operator: "=", value: `$${values.length + 1}` }]], + group_by: [`COALESCE(g.genre, 'No Genre')`], + order_by: "genre", + pageNumber: page, + pageSize: size, + }; + + values.push(libraryid); + + query.values = values; + + const result = await dbHelper.query(query); + + const response = { current_page: page, pages: result.pages, size: size, results: result.results }; + + res.send(response); + } catch (error) { + console.log(error); + res.status(503); + res.send(error); + } +}); + // Handle other routes router.use((req, res) => { res.status(404).send({ error: "Not Found" }); diff --git a/src/pages/components/library-info.jsx b/src/pages/components/library-info.jsx index 9ac9d23..2dbb98e 100644 --- a/src/pages/components/library-info.jsx +++ b/src/pages/components/library-info.jsx @@ -16,6 +16,7 @@ import { Tabs, Tab, Button, ButtonGroup } from "react-bootstrap"; import { Trans } from "react-i18next"; import LibraryOptions from "./library/library-options"; import GlobalStats from "./general/globalStats"; +import GenreLibraryStats from "./library/genre-library-stats.jsx"; function LibraryInfo() { const { LibraryId } = useParams(); @@ -110,6 +111,8 @@ function LibraryInfo() { title={} /> + + {!data.archived && ( diff --git a/src/pages/components/library/genre-library-stats.jsx b/src/pages/components/library/genre-library-stats.jsx new file mode 100644 index 0000000..9f2faa3 --- /dev/null +++ b/src/pages/components/library/genre-library-stats.jsx @@ -0,0 +1,77 @@ +import { useState, useEffect } from "react"; +import { Row, Col } from "react-bootstrap"; +import GenreStatCard from "../statCards/genre-stat-card.jsx"; +import { Trans } from "react-i18next"; +import "../../css/genres.css"; +import Config from "../../../lib/config"; +import axios from "../../../lib/axios_instance"; + +function GenreLibraryStats(props) { + const [data, setData] = useState(); + const [config, setConfig] = useState(); + + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config.getConfig(); + setConfig(newConfig); + } catch (error) { + console.log(error); + } + }; + + const fetchData = async () => { + if (config) { + try { + const itemData = await axios.get(`/stats/getGenreLibraryStats`, { + params: { + libraryid: props.LibraryId, + }, + headers: { + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", + }, + }); + const results = itemData.data.results || []; + setData(results); + } catch (error) { + console.log(error); + } + } + }; + + if (!data) { + fetchData(); + } + + if (!config) { + fetchConfig(); + } + + const intervalId = setInterval(fetchData, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, config, props.LibraryId]); + + if (!data || data.length == 0 || !config) { + return <>; + } + return ( +
+

+ +

+
+ + + + + + + + +
+
+ ); +} + +export default GenreLibraryStats; diff --git a/src/pages/components/user-info/genre-stat-card.jsx b/src/pages/components/statCards/genre-stat-card.jsx similarity index 94% rename from src/pages/components/user-info/genre-stat-card.jsx rename to src/pages/components/statCards/genre-stat-card.jsx index 747a18f..92f5fd0 100644 --- a/src/pages/components/user-info/genre-stat-card.jsx +++ b/src/pages/components/statCards/genre-stat-card.jsx @@ -24,7 +24,9 @@ function GenreStatCard(props) {

{payload[0].payload.genre}

- {props.dataKey == "duration" ? formatTotalWatchTime(payload[0].value) : payload[0].value} {i18next.t("UNITS.PLAYS")} + {props.dataKey == "duration" + ? formatTotalWatchTime(payload[0].value) + : `${payload[0].value} ${i18next.t("UNITS.PLAYS")}`}

); diff --git a/src/pages/components/user-info.jsx b/src/pages/components/user-info.jsx index d3a5d5a..b65992b 100644 --- a/src/pages/components/user-info.jsx +++ b/src/pages/components/user-info.jsx @@ -12,7 +12,7 @@ import { Trans } from "react-i18next"; import baseUrl from "../../lib/baseurl"; import GlobalStats from "./general/globalStats"; import ActivityTimeline from "../activity_time_line"; -import GenreStats from "./user-info/genre-stats.jsx"; +import GenreUserStats from "./user-info/genre-user-stats.jsx"; function UserInfo() { const { UserId } = useParams(); @@ -125,7 +125,7 @@ function UserInfo() { endpoint={"getGlobalUserStats"} title={} /> - + diff --git a/src/pages/components/user-info/genre-stats.jsx b/src/pages/components/user-info/genre-user-stats.jsx similarity index 90% rename from src/pages/components/user-info/genre-stats.jsx rename to src/pages/components/user-info/genre-user-stats.jsx index 50cf427..c2a79bd 100644 --- a/src/pages/components/user-info/genre-stats.jsx +++ b/src/pages/components/user-info/genre-user-stats.jsx @@ -1,12 +1,12 @@ import { useState, useEffect } from "react"; import { Row, Col } from "react-bootstrap"; -import GenreStatCard from "./genre-stat-card.jsx"; +import GenreStatCard from "../statCards/genre-stat-card.jsx"; import { Trans } from "react-i18next"; import "../../css/genres.css"; import Config from "../../../lib/config"; import axios from "../../../lib/axios_instance"; -function GenreStats(props) { +function GenreUserStats(props) { const [data, setData] = useState(); const [config, setConfig] = useState(); @@ -23,7 +23,7 @@ function GenreStats(props) { const fetchData = async () => { if (config) { try { - const itemData = await axios.get(`/stats/getGenreStats`, { + const itemData = await axios.get(`/stats/getGenreUserStats`, { params: { userid: props.UserId, }, @@ -74,4 +74,4 @@ function GenreStats(props) { ); } -export default GenreStats; +export default GenreUserStats;