From 3a85fcdea342351e3aebae6891cbd941f9f5ae4c Mon Sep 17 00:00:00 2001 From: Thegan Govender Date: Sun, 2 Apr 2023 22:15:28 +0200 Subject: [PATCH] Stats Changes Added more stuff to statistics page Playback per hour page has a bug --- backend/db.js | 8 +- backend/init.sql | 111 ++++++-- .../postgres-image}/Dockerfile | 0 .../postgres-image}/commands.txt | 0 .../postgres-image}/init.sql | 0 backend/server.js | 2 +- backend/stats.js | 74 ++++++ docker-compose.yml | 1 + src/pages/components/general/navbar.js | 1 + .../components/statistics/daily-play-count.js | 240 +++++++++--------- .../statistics/play-stats-by-day.js | 159 ++++++++++++ .../statistics/play-stats-by-hour.js | 161 ++++++++++++ src/pages/css/activity/activity-table.css | 2 +- src/pages/css/stats.css | 24 +- src/pages/css/users/user-details.css | 2 +- src/pages/css/users/users.css | 2 +- src/pages/statistics.js | 65 +++-- 17 files changed, 660 insertions(+), 192 deletions(-) rename {postgres-image => backend/postgres-image}/Dockerfile (100%) rename {postgres-image => backend/postgres-image}/commands.txt (100%) rename {postgres-image => backend/postgres-image}/init.sql (100%) create mode 100644 src/pages/components/statistics/play-stats-by-day.js create mode 100644 src/pages/components/statistics/play-stats-by-hour.js diff --git a/backend/db.js b/backend/db.js index d0ab298..e13aac6 100644 --- a/backend/db.js +++ b/backend/db.js @@ -16,11 +16,11 @@ if([_POSTGRES_USER,_POSTGRES_PASSWORD,_POSTGRES_IP,_POSTGRES_PORT].includes(unde } -const development=true; -const _DEV_USER='jfstat'; +const development=false; +const _DEV_USER='postgress'; const _DEV_PASSWORD = '123456'; -const _DEV_IP='10.0.0.99'; -const _DEV_PORT = 32778; +const _DEV_IP='localhost'; +const _DEV_PORT = 5432; const pool = new Pool({ user: (development ? _DEV_USER: _POSTGRES_USER), diff --git a/backend/init.sql b/backend/init.sql index 1def902..415a6fa 100644 --- a/backend/init.sql +++ b/backend/init.sql @@ -5,7 +5,7 @@ -- Dumped from database version 15.2 (Debian 15.2-1.pgdg110+1) -- Dumped by pg_dump version 15.1 --- Started on 2023-04-01 09:50:04 UTC +-- Started on 2023-04-02 20:11:41 UTC SET statement_timeout = 0; SET lock_timeout = 0; @@ -19,7 +19,7 @@ SET client_min_messages = warning; SET row_security = off; -- --- TOC entry 252 (class 1255 OID 49412) +-- TOC entry 254 (class 1255 OID 49412) -- Name: fs_last_library_activity(text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -57,7 +57,7 @@ $$; ALTER FUNCTION public.fs_last_library_activity(libraryid text) OWNER TO postgres; -- --- TOC entry 248 (class 1255 OID 49383) +-- TOC entry 250 (class 1255 OID 49383) -- Name: fs_last_user_activity(text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -93,7 +93,7 @@ $$; ALTER FUNCTION public.fs_last_user_activity(userid text) OWNER TO postgres; -- --- TOC entry 246 (class 1255 OID 49411) +-- TOC entry 248 (class 1255 OID 49411) -- Name: fs_library_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -145,7 +145,7 @@ $$; ALTER FUNCTION public.fs_most_active_user(days integer) OWNER TO postgres; -- --- TOC entry 250 (class 1255 OID 49386) +-- TOC entry 252 (class 1255 OID 49386) -- Name: fs_most_played_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -186,7 +186,7 @@ $$; ALTER FUNCTION public.fs_most_played_items(days integer, itemtype text) OWNER TO postgres; -- --- TOC entry 251 (class 1255 OID 49394) +-- TOC entry 253 (class 1255 OID 49394) -- Name: fs_most_popular_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -256,7 +256,7 @@ $$; ALTER FUNCTION public.fs_most_used_clients(days integer) OWNER TO postgres; -- --- TOC entry 249 (class 1255 OID 49385) +-- TOC entry 251 (class 1255 OID 49385) -- Name: fs_most_viewed_libraries(integer); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -300,7 +300,7 @@ $$; ALTER FUNCTION public.fs_most_viewed_libraries(days integer) OWNER TO postgres; -- --- TOC entry 247 (class 1255 OID 49364) +-- TOC entry 249 (class 1255 OID 49364) -- Name: fs_user_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -325,7 +325,7 @@ $$; ALTER FUNCTION public.fs_user_stats(hours integer, userid text) OWNER TO postgres; -- --- TOC entry 245 (class 1255 OID 49418) +-- TOC entry 246 (class 1255 OID 49418) -- Name: fs_watch_stats_over_time(integer); Type: FUNCTION; Schema: public; Owner: postgres -- @@ -350,6 +350,77 @@ $$; ALTER FUNCTION public.fs_watch_stats_over_time(days integer) OWNER TO postgres; +-- +-- TOC entry 247 (class 1255 OID 57644) +-- Name: fs_watch_stats_popular_days_of_week(integer); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_watch_stats_popular_days_of_week(days integer) RETURNS TABLE("Day" text, "Count" bigint, "Library" text) + LANGUAGE plpgsql + AS $$ +BEGIN +RETURN QUERY +SELECT + TO_CHAR(d."Day", 'Day') AS "Day", + COUNT(a."NowPlayingItemId") AS "Count", + COALESCE(l."Name", 'Unknown') AS "Library" +FROM ( + SELECT + DATE_TRUNC('week', NOW()) + n * INTERVAL '1 day' AS "Day" + FROM generate_series(0, 6) n +) d +CROSS JOIN jf_libraries l +LEFT JOIN ( + SELECT + DATE_TRUNC('day', "ActivityDateInserted") AS "Day", + "NowPlayingItemId", + i."ParentId" + FROM jf_playback_activity a + JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId" + WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW() +) a ON d."Day" = a."Day" AND l."Id" = a."ParentId" +GROUP BY d."Day", l."Name" +ORDER BY d."Day"; + +END; +$$; + + +ALTER FUNCTION public.fs_watch_stats_popular_days_of_week(days integer) OWNER TO postgres; + +-- +-- TOC entry 245 (class 1255 OID 57646) +-- Name: fs_watch_stats_popular_hour_of_day(integer); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_watch_stats_popular_hour_of_day(days integer) RETURNS TABLE("Hour" integer, "Count" integer, "Library" text) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN QUERY + SELECT + DATE_PART('hour', hl."Hour")::INTEGER AS "Hour", + COALESCE(CAST(COUNT(a."NowPlayingItemId") AS INTEGER), 0) AS "Count", + COALESCE(l."Name", hl."Name") AS "Library" + FROM ( + SELECT + DATE_TRUNC('week', NOW()) + INTERVAL '1 hour' * n AS "Hour", + l."Name" + FROM generate_series(0, 167) n + CROSS JOIN jf_libraries l + ) hl + LEFT JOIN jf_playback_activity a ON DATE_TRUNC('hour', a."ActivityDateInserted") = hl."Hour" + LEFT JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId" + LEFT JOIN jf_libraries l ON i."ParentId" = l."Id" + WHERE hl."Hour" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW() + GROUP BY DATE_PART('hour', hl."Hour"), COALESCE(l."Name", hl."Name") + ORDER BY DATE_PART('hour', hl."Hour"), COALESCE(l."Name", hl."Name"); +END; +$$; + + +ALTER FUNCTION public.fs_watch_stats_popular_hour_of_day(days integer) OWNER TO postgres; + SET default_tablespace = ''; SET default_table_access_method = heap; @@ -688,7 +759,7 @@ CREATE VIEW public.js_library_stats_overview AS ALTER TABLE public.js_library_stats_overview OWNER TO postgres; -- --- TOC entry 3237 (class 2606 OID 16401) +-- TOC entry 3239 (class 2606 OID 16401) -- Name: app_config app_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -697,7 +768,7 @@ ALTER TABLE ONLY public.app_config -- --- TOC entry 3239 (class 2606 OID 16419) +-- TOC entry 3241 (class 2606 OID 16419) -- Name: jf_libraries jf_libraries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -706,7 +777,7 @@ ALTER TABLE ONLY public.jf_libraries -- --- TOC entry 3245 (class 2606 OID 24912) +-- TOC entry 3247 (class 2606 OID 24912) -- Name: jf_library_episodes jf_library_episodes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -715,7 +786,7 @@ ALTER TABLE ONLY public.jf_library_episodes -- --- TOC entry 3241 (class 2606 OID 24605) +-- TOC entry 3243 (class 2606 OID 24605) -- Name: jf_library_items jf_library_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -724,7 +795,7 @@ ALTER TABLE ONLY public.jf_library_items -- --- TOC entry 3243 (class 2606 OID 24737) +-- TOC entry 3245 (class 2606 OID 24737) -- Name: jf_library_seasons jf_library_seasons_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -733,7 +804,7 @@ ALTER TABLE ONLY public.jf_library_seasons -- --- TOC entry 3247 (class 2606 OID 41737) +-- TOC entry 3249 (class 2606 OID 41737) -- Name: jf_users jf_users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -742,7 +813,7 @@ ALTER TABLE ONLY public.jf_users -- --- TOC entry 3391 (class 2618 OID 25163) +-- TOC entry 3393 (class 2618 OID 25163) -- Name: jf_library_count_view _RETURN; Type: RULE; Schema: public; Owner: postgres -- @@ -762,7 +833,7 @@ CREATE OR REPLACE VIEW public.jf_library_count_view AS -- --- TOC entry 3248 (class 2606 OID 24617) +-- TOC entry 3250 (class 2606 OID 24617) -- Name: jf_library_items jf_library_items_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- @@ -771,15 +842,15 @@ ALTER TABLE ONLY public.jf_library_items -- --- TOC entry 3399 (class 0 OID 0) --- Dependencies: 3248 +-- TOC entry 3401 (class 0 OID 0) +-- Dependencies: 3250 -- Name: CONSTRAINT jf_library_items_fkey ON jf_library_items; Type: COMMENT; Schema: public; Owner: postgres -- COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items IS 'jf_library'; --- Completed on 2023-04-01 09:50:05 UTC +-- Completed on 2023-04-02 20:11:41 UTC -- -- PostgreSQL database dump complete diff --git a/postgres-image/Dockerfile b/backend/postgres-image/Dockerfile similarity index 100% rename from postgres-image/Dockerfile rename to backend/postgres-image/Dockerfile diff --git a/postgres-image/commands.txt b/backend/postgres-image/commands.txt similarity index 100% rename from postgres-image/commands.txt rename to backend/postgres-image/commands.txt diff --git a/postgres-image/init.sql b/backend/postgres-image/init.sql similarity index 100% rename from postgres-image/init.sql rename to backend/postgres-image/init.sql diff --git a/backend/server.js b/backend/server.js index d92de85..aca9ebd 100644 --- a/backend/server.js +++ b/backend/server.js @@ -12,7 +12,7 @@ const db = require('./db'); const app = express(); const PORT = process.env.PORT || 3003; const LISTEN_IP = '127.0.0.1'; -const JWT_SECRET = process.env.JWT_SECRET ||'my-secret-jwt-key'; +const JWT_SECRET = process.env.JWT_SECRET; if (JWT_SECRET === undefined) { console.log('JWT Secret cannot be undefined'); diff --git a/backend/stats.js b/backend/stats.js index 7ab6a53..ad4a094 100644 --- a/backend/stats.js +++ b/backend/stats.js @@ -320,6 +320,80 @@ const finalData = Object.values(reorganizedData); } }); +router.post("/getViewsByDays", async (req, res) => { + try { + const { days } = req.body; + let _days = days; + if (days=== undefined) { + _days = 30; + } + const { rows } = await db.query( + `select * from fs_watch_stats_popular_days_of_week('${_days}')` + ); + + +const reorganizedData = {}; + +rows.forEach((item) => { + + const id = item.Library; + const count = item.Count; + const day = item.Day; + + if (!reorganizedData[id]) { + reorganizedData[id] = { + id, + data: [] + }; + } + + reorganizedData[id].data.push({ x: day, y: count }); +}); +const finalData = Object.values(reorganizedData); + res.send(finalData); + } catch (error) { + console.log(error); + res.send(error); + } +}); + + +router.post("/getViewsByHour", async (req, res) => { + try { + const { days } = req.body; + let _days = days; + if (days=== undefined) { + _days = 30; + } + const { rows } = await db.query( + `select * from fs_watch_stats_popular_hour_of_day('${_days}')` + ); + + +const reorganizedData = {}; + +rows.forEach((item) => { + + const id = item.Library; + const count = item.Count; + const hour = item.Hour; + + if (!reorganizedData[id]) { + reorganizedData[id] = { + id, + data: [] + }; + } + + reorganizedData[id].data.push({ x: hour, y: count }); +}); +const finalData = Object.values(reorganizedData); + res.send(finalData); + } catch (error) { + console.log(error); + res.send(error); + } +}); diff --git a/docker-compose.yml b/docker-compose.yml index cf5f2a1..3640ed3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: POSTGRES_PASSWORD: mypassword POSTGRES_IP: jellystat-db POSTGRES_PORT: 5432 + JWT_SECRET: 'my-secret-jwt-key' ports: - "3000:3000" depends_on: diff --git a/src/pages/components/general/navbar.js b/src/pages/components/general/navbar.js index c39a739..c04cb00 100644 --- a/src/pages/components/general/navbar.js +++ b/src/pages/components/general/navbar.js @@ -13,6 +13,7 @@ export default function Navbar() { return (
+

Jellystat

{navData.map((item) => { return ( diff --git a/src/pages/components/statistics/daily-play-count.js b/src/pages/components/statistics/daily-play-count.js index 58358d1..6d0a8ed 100644 --- a/src/pages/components/statistics/daily-play-count.js +++ b/src/pages/components/statistics/daily-play-count.js @@ -1,41 +1,37 @@ -import React,{useState,useEffect} from 'react'; -import axios from 'axios'; -import { ResponsiveLine } from '@nivo/line'; +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { ResponsiveLine } from "@nivo/line"; -import '../../css/stats.css'; +import "../../css/stats.css"; function DailyPlayStats(props) { - const [data, setData] = useState(); + const [data, setData] = useState(); const [days, setDays] = useState(60); - const token = localStorage.getItem('token'); - - + const token = localStorage.getItem("token"); useEffect(() => { - - const fetchLibraries = () => { + const url = `/stats/getViewsOverTime`; - const url = `/stats/getViewsOverTime`; - - axios - .post(url, {days:props.days}, { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, + axios + .post( + url, + { days: props.days }, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ) + .then((data) => { + setData(data.data); }) - .then((data) => { - setData(data.data); - }) - .catch((error) => { - console.log(error); - }); - + .catch((error) => { + console.log(error); + }); }; - - if (!data) { fetchLibraries(); } @@ -44,130 +40,120 @@ function DailyPlayStats(props) { fetchLibraries(); } - const intervalId = setInterval(fetchLibraries, 60000 * 5); return () => clearInterval(intervalId); - }, [data, days,props.days ,token]); + }, [data, days, props.days, token]); if (!data) { - return <>; + return <>; } - if (data.length === 0) { - return (
+ return ( +
+

Daily Play Count Per Library - {days} Days

-

Daily Play Count Per Library - {days} Days

- - -

No Stats to display

- -
+

No Stats to display

+
); } + return ( +
+

Daily Play Count Per Library - {days} Days

- return ( -
-

Daily Play Count Per Library - {days} Days

+
-
- ); - + itemTextColor: "#fff", + anchor: "bottom", + direction: "row", + justify: false, + translateX: 0, + translateY: 100, + itemsSpacing: 0, + itemDirection: "left-to-right", + itemWidth: 100, + itemHeight: 20, + itemOpacity: 0.75, + symbolSize: 12, + symbolShape: "circle", + symbolBorderColor: "rgba(0, 0, 0, .5)", + effects: [ + { + on: "hover", + style: { + itemBackground: "rgba(0, 0, 0, .03)", + itemOpacity: 1, + }, + }, + ], + }, + ]} + /> +
+
+ ); } export default DailyPlayStats; - - diff --git a/src/pages/components/statistics/play-stats-by-day.js b/src/pages/components/statistics/play-stats-by-day.js new file mode 100644 index 0000000..c5e86fc --- /dev/null +++ b/src/pages/components/statistics/play-stats-by-day.js @@ -0,0 +1,159 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { ResponsiveLine } from "@nivo/line"; + +import "../../css/stats.css"; + +function PlayStatsByDay(props) { + const [data, setData] = useState(); + const [days, setDays] = useState(60); + const token = localStorage.getItem("token"); + + useEffect(() => { + const fetchLibraries = () => { + const url = `/stats/getViewsByDays`; + + axios + .post( + url, + { days: props.days }, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + }; + + if (!data) { + fetchLibraries(); + } + if (days !== props.days) { + setDays(props.days); + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, days, props.days, token]); + + if (!data) { + return <>; + } + + if (data.length === 0) { + return ( +
+

Play Count By Day - {days} Days

+ +

No Stats to display

+
+ ); + } + + return ( +
+

Play Count By Day - {days} Days

+
+ +
+
+ ); +} + +export default PlayStatsByDay; diff --git a/src/pages/components/statistics/play-stats-by-hour.js b/src/pages/components/statistics/play-stats-by-hour.js new file mode 100644 index 0000000..a3fcc91 --- /dev/null +++ b/src/pages/components/statistics/play-stats-by-hour.js @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { ResponsiveLine } from "@nivo/line"; + +import "../../css/stats.css"; + +function PlayStatsByHour(props) { + const [data, setData] = useState(); + const [days, setDays] = useState(60); + const token = localStorage.getItem("token"); + + useEffect(() => { + const fetchLibraries = () => { + const url = `/stats/getViewsByHour`; + + axios + .post( + url, + { days: props.days }, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + }; + + if (!data) { + fetchLibraries(); + } + if (days !== props.days) { + setDays(props.days); + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, days, props.days, token]); + + if (!data) { + return <>; + } + + if (data.length === 0) { + return ( +
+

Play Count By Hour - {days} Days

+ +

No Stats to display

+
+ ); + } + + console.log(data); + + return ( +
+

Play Count By Hour - {days} Days

+
+ +
+
+ ); +} + +export default PlayStatsByHour; diff --git a/src/pages/css/activity/activity-table.css b/src/pages/css/activity/activity-table.css index 4a18c72..2072dce 100644 --- a/src/pages/css/activity/activity-table.css +++ b/src/pages/css/activity/activity-table.css @@ -16,7 +16,7 @@ div a .activity-table { - background-color: rgba(0, 0, 0, 0.2); + background-color: rgba(100,100, 100, 0.2); } diff --git a/src/pages/css/stats.css b/src/pages/css/stats.css index ee723a8..702579b 100644 --- a/src/pages/css/stats.css +++ b/src/pages/css/stats.css @@ -1,12 +1,32 @@ -.statistics-widget +.graph { height: 700px; color:black !important; - background-color:rgba(0,0,0,0.2); + background-color:rgba(100,100,100,0.2); padding:20px; border-radius:4px; /* text-align: center; */ } + + +.small +{ + height: 500px; + +} + +.statistics-graphs +{ + display: flex; + justify-content: space-between; + +} + +.statistics-widget +{ + flex: 1; + /* margin-right: 20px; */ +} diff --git a/src/pages/css/users/user-details.css b/src/pages/css/users/user-details.css index e801077..9ff2d18 100644 --- a/src/pages/css/users/user-details.css +++ b/src/pages/css/users/user-details.css @@ -23,7 +23,7 @@ height: 100px; border-radius: 50%; object-fit: cover; - box-shadow: 0 0 10px 5px rgba(0,0,0,0.2); + box-shadow: 0 0 10px 5px rgba(100,100,100,0.2); } .user-image-container diff --git a/src/pages/css/users/users.css b/src/pages/css/users/users.css index 39f117f..0fc3463 100644 --- a/src/pages/css/users/users.css +++ b/src/pages/css/users/users.css @@ -13,7 +13,7 @@ font-family: sans-serif; min-width: 400px; /* box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); */ - background-color: rgba(0, 0, 0, 0.2); + background-color: rgba(100,100, 100, 0.2); color: white; width: 100%; diff --git a/src/pages/statistics.js b/src/pages/statistics.js index 0239e5c..4eef201 100644 --- a/src/pages/statistics.js +++ b/src/pages/statistics.js @@ -1,12 +1,13 @@ -import React,{useState} from 'react'; +import React, { useState } from "react"; // import './css/library/libraries.css'; -import "./css/statCard.css"; +import "./css/stats.css"; -import DailyPlayStats from './components/statistics/daily-play-count'; +import DailyPlayStats from "./components/statistics/daily-play-count"; +import PlayStatsByDay from "./components/statistics/play-stats-by-day"; +import PlayStatsByHour from "./components/statistics/play-stats-by-hour"; function Statistics() { - const [days, setDays] = useState(60); const [input, setInput] = useState(60); @@ -23,40 +24,34 @@ function Statistics() { } }; - - return ( -
-
-

Statistics

-
-
Last
-
- setInput(event.target.value)} - onKeyDown={handleKeyDown} - /> -
-
Days
+ return ( +
+
+

Statistics

+
+
Last
+
+ setInput(event.target.value)} + onKeyDown={handleKeyDown} + />
- - -
-
- - - +
Days
- - - - ); - +
+ + +
+ + +
+
+
+ ); } export default Statistics; - -