mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
stat fixes+dynamic user content
Fixed more sql quereis Noticed hardcoded userid value for last-played/recently played stats, changed to dynamic admin userid due to full permission access of libraries
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// api.js
|
||||
const express = require("express");
|
||||
const axios = require("axios");
|
||||
const ActivityMonitor=require('./watchdog/ActivityMonitor');
|
||||
const db = require("./db");
|
||||
|
||||
@@ -115,6 +116,28 @@ router.get("/getHistory", async (req, res) => {
|
||||
|
||||
});
|
||||
|
||||
router.get("/getAdminUsers", async (req, res) => {
|
||||
try {
|
||||
const { rows:config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
const url = `${config[0].JF_HOST}/Users`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": config[0].JF_API_KEY,
|
||||
},
|
||||
});
|
||||
const adminUser = await response.data.filter(
|
||||
(user) => user.Policy.IsAdministrator === true
|
||||
);
|
||||
res.send(adminUser);
|
||||
} catch (error) {
|
||||
console.log( error);
|
||||
res.send(error);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
router.get("/runWatchdog", async (req, res) => {
|
||||
let message='Watchdog Started';
|
||||
if(!process.env.WatchdogRunning )
|
||||
@@ -129,4 +152,7 @@ router.get("/runWatchdog", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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-04 12:49:03 UTC
|
||||
-- Started on 2023-04-05 21:14:53 UTC
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
@@ -19,7 +19,7 @@ SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- TOC entry 254 (class 1255 OID 49412)
|
||||
-- TOC entry 253 (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 250 (class 1255 OID 49383)
|
||||
-- TOC entry 249 (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 247 (class 1255 OID 49411)
|
||||
-- TOC entry 246 (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 252 (class 1255 OID 49386)
|
||||
-- TOC entry 251 (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 253 (class 1255 OID 49394)
|
||||
-- TOC entry 252 (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 251 (class 1255 OID 49385)
|
||||
-- TOC entry 250 (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 249 (class 1255 OID 49364)
|
||||
-- TOC entry 248 (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 248 (class 1255 OID 49418)
|
||||
-- TOC entry 247 (class 1255 OID 49418)
|
||||
-- Name: fs_watch_stats_over_time(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -370,7 +370,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_watch_stats_over_time(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 246 (class 1255 OID 57644)
|
||||
-- TOC entry 254 (class 1255 OID 57644)
|
||||
-- Name: fs_watch_stats_popular_days_of_week(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -379,27 +379,49 @@ CREATE FUNCTION public.fs_watch_stats_popular_days_of_week(days integer) RETURNS
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH library_days AS (
|
||||
SELECT
|
||||
l."Name" AS "Library",
|
||||
d.day_of_week,
|
||||
d.day_name
|
||||
FROM
|
||||
jf_libraries l,
|
||||
(SELECT 0 AS "day_of_week", 'Sunday' AS "day_name" UNION ALL
|
||||
SELECT 1 AS "day_of_week", 'Monday' AS "day_name" UNION ALL
|
||||
SELECT 2 AS "day_of_week", 'Tuesday' AS "day_name" UNION ALL
|
||||
SELECT 3 AS "day_of_week", 'Wednesday' AS "day_name" UNION ALL
|
||||
SELECT 4 AS "day_of_week", 'Thursday' AS "day_name" UNION ALL
|
||||
SELECT 5 AS "day_of_week", 'Friday' AS "day_name" UNION ALL
|
||||
SELECT 6 AS "day_of_week", 'Saturday' AS "day_name"
|
||||
) d
|
||||
)
|
||||
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";
|
||||
library_days.day_name AS "Day",
|
||||
COALESCE(SUM(counts."Count"), 0)::bigint AS "Count",
|
||||
library_days."Library" AS "Library"
|
||||
|
||||
FROM
|
||||
library_days
|
||||
LEFT JOIN
|
||||
(SELECT
|
||||
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
|
||||
COUNT(*) AS "Count",
|
||||
EXTRACT(DOW FROM a."ActivityDateInserted") AS "DOW",
|
||||
l."Name" AS "Library"
|
||||
FROM
|
||||
jf_playback_activity a
|
||||
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
||||
JOIN jf_libraries l ON i."ParentId" = l."Id"
|
||||
WHERE
|
||||
a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
|
||||
GROUP BY
|
||||
l."Name", EXTRACT(DOW FROM a."ActivityDateInserted"), DATE_TRUNC('day', a."ActivityDateInserted")
|
||||
) counts
|
||||
ON counts."DOW" = library_days.day_of_week AND counts."Library" = library_days."Library"
|
||||
GROUP BY
|
||||
library_days.day_name, library_days.day_of_week, library_days."Library"
|
||||
ORDER BY
|
||||
library_days.day_of_week, library_days."Library";
|
||||
|
||||
END;
|
||||
$$;
|
||||
@@ -874,7 +896,7 @@ ALTER TABLE ONLY public.jf_library_items
|
||||
COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items IS 'jf_library';
|
||||
|
||||
|
||||
-- Completed on 2023-04-04 12:49:04 UTC
|
||||
-- Completed on 2023-04-05 21:14:58 UTC
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
|
||||
@@ -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;
|
||||
const JWT_SECRET = process.env.JWT_SECRET ||'my-secret-jwt-key';
|
||||
|
||||
if (JWT_SECRET === undefined) {
|
||||
console.log('JWT Secret cannot be undefined');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
main{
|
||||
padding-inline: 10px;
|
||||
margin-inline: 40px;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
|
||||
3
src/lib/devices.js
Normal file
3
src/lib/devices.js
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
export const clientData = ["android","ios","safari","chrome","firefox","edge"]
|
||||
|
||||
@@ -51,7 +51,6 @@ function LibraryLastPlayed(props) {
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data,config, props.LibraryId]);
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (!data || !config) {
|
||||
return <></>;
|
||||
@@ -62,7 +61,7 @@ function LibraryLastPlayed(props) {
|
||||
<h1>Last Watched</h1>
|
||||
<div className="last-played-container">
|
||||
{data.map((item) => (
|
||||
<ItemCardInfo data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
|
||||
<ItemCardInfo data={item} base_url={config.hostUrl} key={item.Id+item.SeasonNumber+item.EpisodeNumber}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -21,23 +21,39 @@ function RecentlyPlayed(props) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAdmin = async () => {
|
||||
try {
|
||||
let url=`/api/getAdminUsers`;
|
||||
const adminData = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return adminData.data[0].Id;
|
||||
// setData(itemData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let url=`${config.hostUrl}/users/5f63950a2339462196eb8cead70cae7e/Items/latest?parentId=${props.LibraryId}`;
|
||||
let adminId=await fetchAdmin();
|
||||
let url=`${config.hostUrl}/users/${adminId}/Items/latest?parentId=${props.LibraryId}`;
|
||||
const itemData = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
console.log(itemData);
|
||||
setData(itemData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
if (!data && config) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
@@ -49,7 +65,6 @@ function RecentlyPlayed(props) {
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data,config, props.LibraryId]);
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (!data || !config) {
|
||||
return <></>;
|
||||
|
||||
@@ -5,6 +5,8 @@ import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
import PlayFillIcon from "remixicon-react/PlayFillIcon";
|
||||
import PauseFillIcon from "remixicon-react/PauseFillIcon";
|
||||
|
||||
import { clientData } from "../../../lib/devices";
|
||||
|
||||
function ticksToTimeString(ticks) {
|
||||
// Convert ticks to seconds
|
||||
const seconds = Math.floor(ticks / 10000000);
|
||||
@@ -37,24 +39,18 @@ function sessionCard(props) {
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/web/assets/img/devices/" +
|
||||
(props.data.session.DeviceName.toLowerCase().includes("ios") ||
|
||||
props.data.session.Client.toLowerCase().includes("ios")
|
||||
? "ios"
|
||||
: props.data.session.DeviceName.toLowerCase().includes(
|
||||
"android"
|
||||
) ||
|
||||
props.data.session.Client.toLowerCase().includes("android")
|
||||
? "android"
|
||||
: props.data.session.DeviceName.replace(
|
||||
" ",
|
||||
""
|
||||
).toLowerCase()) +
|
||||
+
|
||||
(props.data.session.Client.toLowerCase().includes("web") ?
|
||||
( clientData.find(item => props.data.session.DeviceName.toLowerCase().includes(item)) || "other")
|
||||
:
|
||||
( clientData.find(item => props.data.session.Client.toLowerCase().includes(item)) || "other")
|
||||
)
|
||||
+
|
||||
".svg"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
<div className="card-device-name">
|
||||
{" "}
|
||||
{props.data.session.DeviceName}
|
||||
</div>
|
||||
<div className="card-client">
|
||||
@@ -133,19 +129,14 @@ function sessionCard(props) {
|
||||
className="card-device-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/web/assets/img/devices/" +
|
||||
(props.data.session.DeviceName.toLowerCase().includes("ios") ||
|
||||
props.data.session.Client.toLowerCase().includes("ios")
|
||||
? "ios"
|
||||
: props.data.session.DeviceName.toLowerCase().includes(
|
||||
"android"
|
||||
) ||
|
||||
props.data.session.Client.toLowerCase().includes("android")
|
||||
? "android"
|
||||
: props.data.session.DeviceName.replace(
|
||||
" ",
|
||||
""
|
||||
).toLowerCase()) +
|
||||
"/web/assets/img/devices/"
|
||||
+
|
||||
(props.data.session.Client.toLowerCase().includes("web") ?
|
||||
( clientData.find(item => props.data.session.DeviceName.toLowerCase().includes(item)) || "other")
|
||||
:
|
||||
( clientData.find(item => props.data.session.Client.toLowerCase().includes(item)) || "other")
|
||||
)
|
||||
+
|
||||
".svg"
|
||||
}
|
||||
alt=""
|
||||
|
||||
@@ -50,7 +50,7 @@ function DailyPlayStats(props) {
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="statistics-widget">
|
||||
<div className="main-widget">
|
||||
<h1>Daily Play Count Per Library - {days} Days</h1>
|
||||
|
||||
<h1>No Stats to display</h1>
|
||||
@@ -59,17 +59,17 @@ function DailyPlayStats(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="statistics-widget">
|
||||
<h1>Daily Play Count Per Library - {days} Days</h1>
|
||||
<div className="main-widget">
|
||||
<h1>Daily Play Count Per Library - Last {days} Days</h1>
|
||||
|
||||
<div className="graph">
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
margin={{ top: 50, right: 100, bottom: 200, left: 50 }}
|
||||
margin={{ top: 50, right: 40, bottom: 100, left: 50 }}
|
||||
xScale={{ type: "point" }}
|
||||
yScale={{
|
||||
type: "linear",
|
||||
min: "auto",
|
||||
min: 0,
|
||||
max: "auto",
|
||||
stacked: false,
|
||||
reverse: false,
|
||||
|
||||
@@ -51,7 +51,7 @@ function PlayStatsByDay(props) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="statistics-widget small">
|
||||
<h1>Play Count By Day - {days} Days</h1>
|
||||
<h1>Play Count By Day - Last {days} Days</h1>
|
||||
|
||||
<h1>No Stats to display</h1>
|
||||
</div>
|
||||
@@ -60,15 +60,15 @@ function PlayStatsByDay(props) {
|
||||
|
||||
return (
|
||||
<div className="statistics-widget">
|
||||
<h1>Play Count By Day - {days} Days</h1>
|
||||
<h1>Play Count By Day - Last {days} Days</h1>
|
||||
<div className="graph small">
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
margin={{ top: 50, right: 100, bottom: 200, left: 50 }}
|
||||
margin={{ top: 50, right: 40, bottom: 100, left: 50 }}
|
||||
xScale={{ type: "point" }}
|
||||
yScale={{
|
||||
type: "linear",
|
||||
min: "auto",
|
||||
min: 0,
|
||||
max: "auto",
|
||||
stacked: false,
|
||||
reverse: false,
|
||||
@@ -101,7 +101,7 @@ function PlayStatsByDay(props) {
|
||||
orient: "bottom",
|
||||
tickSize: 5,
|
||||
tickPadding: 10,
|
||||
tickRotation: -45,
|
||||
tickRotation: 0,
|
||||
legend: "Days",
|
||||
legendOffset: 36,
|
||||
legendPosition: "middle",
|
||||
|
||||
@@ -51,7 +51,7 @@ function PlayStatsByHour(props) {
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="statistics-widget small">
|
||||
<h1>Play Count By Hour - {days} Days</h1>
|
||||
<h1>Play Count By Hour - Last {days} Days</h1>
|
||||
|
||||
<h1>No Stats to display</h1>
|
||||
</div>
|
||||
@@ -62,15 +62,15 @@ function PlayStatsByHour(props) {
|
||||
|
||||
return (
|
||||
<div className="statistics-widget">
|
||||
<h1>Play Count By Hour - {days} Days</h1>
|
||||
<h1>Play Count By Hour - Last {days} Days</h1>
|
||||
<div className="graph small">
|
||||
<ResponsiveLine
|
||||
data={data}
|
||||
margin={{ top: 50, right: 100, bottom: 200, left: 50 }}
|
||||
margin={{ top: 50, right: 40, bottom: 100, left: 50 }}
|
||||
xScale={{ type: "point" }}
|
||||
yScale={{
|
||||
type: "linear",
|
||||
min: "auto",
|
||||
min: 0,
|
||||
max: "auto",
|
||||
stacked: false,
|
||||
reverse: false,
|
||||
@@ -103,7 +103,7 @@ function PlayStatsByHour(props) {
|
||||
orient: "bottom",
|
||||
tickSize: 5,
|
||||
tickPadding: 10,
|
||||
tickRotation: -45,
|
||||
tickRotation: 0,
|
||||
legend: "Days",
|
||||
legendOffset: 36,
|
||||
legendPosition: "middle",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
height: 700px;
|
||||
color:black !important;
|
||||
background-color:rgba(100,100,100,0.2);
|
||||
padding:20px;
|
||||
padding:10px;
|
||||
|
||||
border-radius:4px;
|
||||
/* text-align: center; */
|
||||
@@ -22,11 +22,36 @@
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.main-widget
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.statistics-widget
|
||||
{
|
||||
flex: 1;
|
||||
max-width: 49.5%;
|
||||
/* margin-right: 20px; */
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.chart-canvas {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.chart-canvas {
|
||||
height: 600px;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import PlayStatsByHour from "./components/statistics/play-stats-by-hour";
|
||||
|
||||
function Statistics() {
|
||||
const [days, setDays] = useState(60);
|
||||
const [input, setInput] = useState(60);
|
||||
const [input, setInput] = useState(60);
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
|
||||
Reference in New Issue
Block a user