From cf0e85d9344838207ea0d2813507d5ea0da0c506 Mon Sep 17 00:00:00 2001 From: CyferShepard Date: Fri, 2 Aug 2024 14:58:49 +0200 Subject: [PATCH] feat: Added Playback concurrent stream card fixed translation key typo in stream-info modal --- backend/routes/stats.js | 362 +++++++++++------- public/locales/en-UK/translation.json | 6 +- src/pages/components/HomeStatisticCards.jsx | 24 +- src/pages/components/activity/stream_info.jsx | 2 +- .../statCards/ItemStatComponent.jsx | 178 ++++----- .../statCards/playback_method_stats.jsx | 91 +++++ .../statistics/play-method-chart.jsx | 91 +++++ .../statistics/playbackMethodStats.jsx | 84 ++++ src/pages/testing.jsx | 4 + 9 files changed, 591 insertions(+), 251 deletions(-) create mode 100644 src/pages/components/statCards/playback_method_stats.jsx create mode 100644 src/pages/components/statistics/play-method-chart.jsx create mode 100644 src/pages/components/statistics/playbackMethodStats.jsx diff --git a/backend/routes/stats.js b/backend/routes/stats.js index 9da6c6c..3143818 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -1,10 +1,39 @@ // api.js const express = require("express"); const db = require("../db"); +const moment = require("moment"); const router = express.Router(); +//functions +function countOverlapsPerHour(records) { + const hourCounts = {}; + records.forEach((record) => { + const start = moment(record.StartTime).subtract(1, "hour"); + const end = moment(record.EndTime).add(1, "hour"); + + // Iterate through each hour from start to end + for (let hour = start.clone().startOf("hour"); hour.isBefore(end); hour.add(1, "hour")) { + const hourKey = hour.format("MMM DD, YY HH:00"); + if (!hourCounts[hourKey]) { + hourCounts[hourKey] = { Transcodes: 0, DirectPlays: 0 }; + } + if (record.PlayMethod === "Transcode") { + hourCounts[hourKey].Transcodes++; + } else { + hourCounts[hourKey].DirectPlays++; + } + } + }); + + // Convert the hourCounts object to an array of key-value pairs, sort it, and convert it back to an object + const sortedHourCounts = Object.fromEntries(Object.entries(hourCounts).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))); + + return sortedHourCounts; +} + +//endpoints router.get("/getLibraryOverview", async (req, res) => { try { @@ -18,35 +47,29 @@ router.get("/getLibraryOverview", async (req, res) => { router.post("/getMostViewedByType", async (req, res) => { try { - const { days,type } = req.body; + const { days, type } = req.body; - const valid_types=['Audio','Movie','Series']; + const valid_types = ["Audio", "Movie", "Series"]; let _days = days; if (days === undefined) { _days = 30; } - if(!valid_types.includes(type)) - { + if (!valid_types.includes(type)) { res.status(503); return res.send(`Invalid Type Value.\nValid Types: ${JSON.stringify(valid_types)}`); } - if(isNaN(parseFloat(days))) - { + if (isNaN(parseFloat(days))) { res.status(503); return res.send(`Days needs to be a number.`); } - if(Number(days)<0) - { + if (Number(days) < 0) { res.status(503); return res.send(`Days cannot be less than 0`); } - - const { rows } = await db.query( - `select * from fs_most_played_items($1,'${type}') limit 5`, [_days-1] - ); + const { rows } = await db.query(`select * from fs_most_played_items($1,'${type}') limit 5`, [_days - 1]); res.send(rows); } catch (error) { res.status(503); @@ -56,24 +79,21 @@ router.post("/getMostViewedByType", async (req, res) => { router.post("/getMostPopularByType", async (req, res) => { try { - const { days,type } = req.body; + const { days, type } = req.body; - const valid_types=['Audio','Movie','Series']; + const valid_types = ["Audio", "Movie", "Series"]; let _days = days; if (days === undefined) { _days = 30; } - if(!valid_types.includes(type)) - { + if (!valid_types.includes(type)) { res.status(503); - return res.send('Invalid Type Value'); + return res.send("Invalid Type Value"); } - const { rows } = await db.query( - `select * from fs_most_popular_items($1,$2) limit 5`, [_days-1, type] - ); + const { rows } = await db.query(`select * from fs_most_popular_items($1,$2) limit 5`, [_days - 1, type]); res.send(rows); } catch (error) { res.status(503); @@ -81,8 +101,6 @@ router.post("/getMostPopularByType", async (req, res) => { } }); - - router.post("/getMostViewedLibraries", async (req, res) => { try { const { days } = req.body; @@ -90,9 +108,7 @@ router.post("/getMostViewedLibraries", async (req, res) => { if (days === undefined) { _days = 30; } - const { rows } = await db.query( - `select * from fs_most_viewed_libraries($1)`, [_days-1] - ); + const { rows } = await db.query(`select * from fs_most_viewed_libraries($1)`, [_days - 1]); res.send(rows); } catch (error) { res.status(503); @@ -107,9 +123,7 @@ router.post("/getMostUsedClient", async (req, res) => { if (days === undefined) { _days = 30; } - const { rows } = await db.query( - `select * from fs_most_used_clients($1) limit 5`, [_days-1] - ); + const { rows } = await db.query(`select * from fs_most_used_clients($1) limit 5`, [_days - 1]); res.send(rows); } catch (error) { res.status(503); @@ -124,17 +138,14 @@ router.post("/getMostActiveUsers", async (req, res) => { if (days === undefined) { _days = 30; } - const { rows } = await db.query( - `select * from fs_most_active_user($1) limit 5`, [_days-1] - ); - res.send(rows); + const { rows } = await db.query(`select * from fs_most_active_user($1) limit 5`, [_days - 1]); + res.send(rows); } catch (error) { res.status(503); res.send(error); } }); - router.get("/getPlaybackActivity", async (req, res) => { try { const { rows } = await db.query("SELECT * FROM jf_playback_activity"); @@ -154,13 +165,10 @@ router.get("/getAllUserActivity", async (req, res) => { } }); - router.post("/getUserLastPlayed", async (req, res) => { try { const { userid } = req.body; - const { rows } = await db.query( - `select * from fs_last_user_activity($1) limit 15`, [userid] - ); + const { rows } = await db.query(`select * from fs_last_user_activity($1) limit 15`, [userid]); res.send(rows); } catch (error) { console.log(error); @@ -172,14 +180,12 @@ router.post("/getUserLastPlayed", async (req, res) => { //Global Stats router.post("/getGlobalUserStats", async (req, res) => { try { - const { hours,userid } = req.body; + const { hours, userid } = req.body; let _hours = hours; if (hours === undefined) { _hours = 24; } - const { rows } = await db.query( - `select * from fs_user_stats($1,$2)`, [_hours, userid] - ); + const { rows } = await db.query(`select * from fs_user_stats($1,$2)`, [_hours, userid]); res.send(rows[0]); } catch (error) { console.log(error); @@ -190,7 +196,7 @@ router.post("/getGlobalUserStats", async (req, res) => { router.post("/getGlobalItemStats", async (req, res) => { try { - const { hours,itemid } = req.body; + const { hours, itemid } = req.body; let _hours = hours; if (hours === undefined) { _hours = 24; @@ -201,7 +207,8 @@ router.post("/getGlobalItemStats", async (req, res) => { from jf_playback_activity jf_playback_activity where ("EpisodeId"=$1 OR "SeasonId"=$1 OR "NowPlayingItemId"=$1) - AND jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - INTERVAL '1 hour' * $2 AND NOW();`, [itemid, _hours] + AND jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - INTERVAL '1 hour' * $2 AND NOW();`, + [itemid, _hours] ); res.send(rows[0]); } catch (error) { @@ -213,14 +220,12 @@ router.post("/getGlobalItemStats", async (req, res) => { router.post("/getGlobalLibraryStats", async (req, res) => { try { - const { hours,libraryid } = req.body; + const { hours, libraryid } = req.body; let _hours = hours; if (hours === undefined) { _hours = 24; } - const { rows } = await db.query( - `select * from fs_library_stats($1,$2)`, [_hours, libraryid] - ); + const { rows } = await db.query(`select * from fs_library_stats($1,$2)`, [_hours, libraryid]); res.send(rows[0]); } catch (error) { console.log(error); @@ -241,16 +246,13 @@ router.get("/getLibraryCardStats", async (req, res) => { router.post("/getLibraryCardStats", async (req, res) => { try { - const {libraryid } = req.body; - if(libraryid === undefined) - { + const { libraryid } = req.body; + if (libraryid === undefined) { res.status(503); - return res.send('Invalid Library Id'); + return res.send("Invalid Library Id"); } - const { rows } = await db.query( - `select * from js_library_stats_overview where "Id"=$1`, [libraryid] - ); + const { rows } = await db.query(`select * from js_library_stats_overview where "Id"=$1`, [libraryid]); res.send(rows[0]); } catch (error) { console.log(error); @@ -259,8 +261,6 @@ router.post("/getLibraryCardStats", async (req, res) => { } }); - - router.get("/getLibraryMetadata", async (req, res) => { try { const { rows } = await db.query("select * from js_library_metadata"); @@ -272,29 +272,125 @@ router.get("/getLibraryMetadata", async (req, res) => { }); router.post("/getLibraryItemsWithStats", async (req, res) => { - try{ - const {libraryid} = req.body; - const { rows } = await db.query( - `SELECT * FROM jf_library_items_with_playcount_playtime where "ParentId"=$1`, [libraryid] - ); + try { + const { libraryid } = req.body; + const { rows } = await db.query(`SELECT * FROM jf_library_items_with_playcount_playtime where "ParentId"=$1`, [libraryid]); res.send(rows); - - - }catch(error) - { + } catch (error) { console.log(error); } - - }); +router.post("/getLibraryItemsPlayMethodStats", async (req, res) => { + try { + let { libraryid, startDate, endDate = moment(), hours = 24 } = req.body; + + // Validate startDate and endDate using moment + if ( + startDate !== undefined && + (!moment(startDate, moment.ISO_8601, true).isValid() || !moment(endDate, moment.ISO_8601, true).isValid()) + ) { + return res.status(400).send({ error: "Invalid date format" }); + } + + if (hours < 1) { + return res.status(400).send({ error: "Hours cannot be less than 1" }); + } + + if (libraryid === undefined) { + return res.status(400).send({ error: "Invalid Library Id" }); + } + + if (startDate === undefined) { + startDate = moment(endDate).subtract(hours, "hour").format("YYYY-MM-DD HH:mm:ss"); + } + + const { rows } = await db.query( + `select a.*,i."ParentId" + from jf_playback_activity a + left + join jf_library_episodes e + on a."EpisodeId"=e."EpisodeId" + join jf_library_items i + on i."Id"=a."NowPlayingItemId" or e."SeriesId"=i."Id" + where i."ParentId"=$1 + and a."ActivityDateInserted" BETWEEN $2 AND $3 + order by a."ActivityDateInserted" desc; + `, + [libraryid, startDate, endDate] + ); + + const stats = rows.map((item) => { + return { + Id: item.NowPlayingItemId, + UserId: item.UserId, + UserName: item.UserName, + Client: item.Client, + DeviceName: item.DeviceName, + NowPlayingItemName: item.NowPlayingItemName, + EpisodeId: item.EpisodeId || null, + SeasonId: item.SeasonId || null, + StartTime: moment(item.ActivityDateInserted).subtract(item.PlaybackDuration, "seconds").format("YYYY-MM-DD HH:mm:ss"), + EndTime: moment(item.ActivityDateInserted).format("YYYY-MM-DD HH:mm:ss"), + PlaybackDuration: item.PlaybackDuration, + PlayMethod: item.PlayMethod, + TranscodedVideo: item.TranscodingInfo?.IsVideoDirect || false, + TranscodedAudio: item.TranscodingInfo?.IsAudioDirect || false, + ParentId: item.ParentId, + }; + }); + + let countedstats = countOverlapsPerHour(stats); + + let hoursRes = { + types: [ + { Id: "Transcodes", Name: "Transcodes" }, + { Id: "DirectPlays", Name: "DirectPlays" }, + ], + + stats: Object.keys(countedstats).map((key) => { + return { + Key: key, + Transcodes: countedstats[key].Transcodes, + DirectPlays: countedstats[key].DirectPlays, + }; + }), + }; + res.send(hoursRes); + } catch (error) { + console.log(error); + res.send(error); + } +}); + +router.post("/getPlaybackMethodStats", async (req, res) => { + try { + const { days } = req.body; + let _days = days; + if (days === undefined) { + _days = 30; + } + + const { rows } = await db.query( + `select a."PlayMethod" "Name",count(a."PlayMethod") "Count" + from jf_playback_activity a + WHERE a."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => $1) AND NOW() + Group by a."PlayMethod" + `, + [_days] + ); + + res.send(rows); + } catch (error) { + console.log(error); + res.send(error); + } +}); router.post("/getLibraryLastPlayed", async (req, res) => { try { const { libraryid } = req.body; - const { rows } = await db.query( - `select * from fs_last_library_activity($1) limit 15`, [libraryid] - ); + const { rows } = await db.query(`select * from fs_last_library_activity($1) limit 15`, [libraryid]); res.send(rows); } catch (error) { console.log(error); @@ -303,44 +399,37 @@ router.post("/getLibraryLastPlayed", async (req, res) => { } }); - router.post("/getViewsOverTime", async (req, res) => { try { const { days } = req.body; let _days = days; - if (days=== undefined) { + if (days === undefined) { _days = 30; } - const { rows:stats } = await db.query( - `select * from fs_watch_stats_over_time($1)`, [_days] - ); + const { rows: stats } = await db.query(`select * from fs_watch_stats_over_time($1)`, [_days]); - const { rows:libraries } = await db.query( - `select distinct "Id","Name" from jf_libraries` - ); + const { rows: libraries } = await db.query(`select distinct "Id","Name" from jf_libraries`); - -const reorganizedData = {}; + const reorganizedData = {}; -stats.forEach((item) => { - const library = item.Library; - const count = item.Count; - const date = new Date(item.Date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: '2-digit' - }); + stats.forEach((item) => { + const library = item.Library; + const count = item.Count; + const date = new Date(item.Date).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "2-digit", + }); + if (!reorganizedData[date]) { + reorganizedData[date] = { + Key: date, + }; + } - if (!reorganizedData[date]) { - reorganizedData[date] = { - Key:date - }; - } - - reorganizedData[date]= { ...reorganizedData[date], [library]: count}; -}); -const finalData = {libraries:libraries,stats:Object.values(reorganizedData)}; + reorganizedData[date] = { ...reorganizedData[date], [library]: count }; + }); + const finalData = { libraries: libraries, stats: Object.values(reorganizedData) }; res.send(finalData); } catch (error) { console.log(error); @@ -353,35 +442,29 @@ router.post("/getViewsByDays", async (req, res) => { try { const { days } = req.body; let _days = days; - if (days=== undefined) { + if (days === undefined) { _days = 30; } - const { rows:stats } = await db.query( - `select * from fs_watch_stats_popular_days_of_week($1)`, [_days] - ); + const { rows: stats } = await db.query(`select * from fs_watch_stats_popular_days_of_week($1)`, [_days]); - const { rows:libraries } = await db.query( - `select distinct "Id","Name" from jf_libraries` - ); + const { rows: libraries } = await db.query(`select distinct "Id","Name" from jf_libraries`); - -const reorganizedData = {}; + const reorganizedData = {}; -stats.forEach((item) => { - const library = item.Library; - const count = item.Count; - const day = item.Day; + stats.forEach((item) => { + const library = item.Library; + const count = item.Count; + const day = item.Day; + if (!reorganizedData[day]) { + reorganizedData[day] = { + Key: day, + }; + } - if (!reorganizedData[day]) { - reorganizedData[day] = { - Key:day - }; - } - - reorganizedData[day]= { ...reorganizedData[day], [library]: count}; -}); -const finalData = {libraries:libraries,stats:Object.values(reorganizedData)}; + reorganizedData[day] = { ...reorganizedData[day], [library]: count }; + }); + const finalData = { libraries: libraries, stats: Object.values(reorganizedData) }; res.send(finalData); } catch (error) { console.log(error); @@ -394,35 +477,29 @@ router.post("/getViewsByHour", async (req, res) => { try { const { days } = req.body; let _days = days; - if (days=== undefined) { + if (days === undefined) { _days = 30; } - const { rows:stats } = await db.query( - `select * from fs_watch_stats_popular_hour_of_day($1)`, [_days] - ); + const { rows: stats } = await db.query(`select * from fs_watch_stats_popular_hour_of_day($1)`, [_days]); - const { rows:libraries } = await db.query( - `select distinct "Id","Name" from jf_libraries` - ); + const { rows: libraries } = await db.query(`select distinct "Id","Name" from jf_libraries`); - -const reorganizedData = {}; + const reorganizedData = {}; -stats.forEach((item) => { - const library = item.Library; - const count = item.Count; - const hour = item.Hour; + stats.forEach((item) => { + const library = item.Library; + const count = item.Count; + const hour = item.Hour; + if (!reorganizedData[hour]) { + reorganizedData[hour] = { + Key: hour, + }; + } - if (!reorganizedData[hour]) { - reorganizedData[hour] = { - Key:hour - }; - } - - reorganizedData[hour]= { ...reorganizedData[hour], [library]: count}; -}); -const finalData = {libraries:libraries,stats:Object.values(reorganizedData)}; + reorganizedData[hour] = { ...reorganizedData[hour], [library]: count }; + }); + const finalData = { libraries: libraries, stats: Object.values(reorganizedData) }; res.send(finalData); } catch (error) { console.log(error); @@ -431,7 +508,4 @@ const finalData = {libraries:libraries,stats:Object.values(reorganizedData)}; } }); - - - module.exports = router; diff --git a/public/locales/en-UK/translation.json b/public/locales/en-UK/translation.json index afc0c07..ed3522e 100644 --- a/public/locales/en-UK/translation.json +++ b/public/locales/en-UK/translation.json @@ -28,7 +28,8 @@ "MOST_POPULAR_MUSIC": "MOST POPULAR MUSIC", "MOST_VIEWED_LIBRARIES": "MOST VIEWED LIBRARIES", "MOST_USED_CLIENTS": "MOST USED CLIENTS", - "MOST_ACTIVE_USERS": "MOST ACTIVE USERS" + "MOST_ACTIVE_USERS": "MOST ACTIVE USERS", + "CONCURRENT_STREAMS": "CONCURRENT STREAMS" }, "LIBRARY_OVERVIEW": { "MOVIE_LIBRARIES": "MOVIE LIBRARIES", @@ -143,7 +144,8 @@ "SECOND": "Second", "SECONDS": "Seconds", "PLAYS": "Plays", - "ITEMS": "Items" + "ITEMS": "Items", + "STREAMS": "Streams" }, "USERS_PAGE": { "ALL_USERS": "All Users", diff --git a/src/pages/components/HomeStatisticCards.jsx b/src/pages/components/HomeStatisticCards.jsx index cee2d28..85ae773 100644 --- a/src/pages/components/HomeStatisticCards.jsx +++ b/src/pages/components/HomeStatisticCards.jsx @@ -12,6 +12,7 @@ import MPMusic from "./statCards/mp_music"; import "../css/statCard.css"; import { Trans } from "react-i18next"; +import PlaybackMethodStats from "./statCards/playback_method_stats"; function HomeStatisticCards() { const [days, setDays] = useState(30); @@ -32,9 +33,13 @@ function HomeStatisticCards() { return (
-

+

+ +

-
+
+ +
-
+
+ +
- -
- - + + - - -
+ +
); } diff --git a/src/pages/components/activity/stream_info.jsx b/src/pages/components/activity/stream_info.jsx index 0f47a25..7ad9408 100644 --- a/src/pages/components/activity/stream_info.jsx +++ b/src/pages/components/activity/stream_info.jsx @@ -101,7 +101,7 @@ function Row(logs) { - + {data.MediaStreams ? data.MediaStreams.find(stream => stream.Type === 'Video')?.AspectRatio : '-'} {data.MediaStreams ? data.MediaStreams.find(stream => stream.Type === 'Video')?.AspectRatio : '-'} diff --git a/src/pages/components/statCards/ItemStatComponent.jsx b/src/pages/components/statCards/ItemStatComponent.jsx index e088466..30e1934 100644 --- a/src/pages/components/statCards/ItemStatComponent.jsx +++ b/src/pages/components/statCards/ItemStatComponent.jsx @@ -1,90 +1,88 @@ /* eslint-disable react/prop-types */ -import {useState} from "react"; -import { Blurhash } from 'react-blurhash'; +import { useState } from "react"; +import { Blurhash } from "react-blurhash"; import { Link } from "react-router-dom"; -import Card from 'react-bootstrap/Card'; -import Row from 'react-bootstrap/Row'; -import Col from 'react-bootstrap/Col'; -import Tooltip from "@mui/material/Tooltip"; -import ArchiveDrawerFillIcon from 'remixicon-react/ArchiveDrawerFillIcon'; +import Card from "react-bootstrap/Card"; +import Row from "react-bootstrap/Row"; +import Col from "react-bootstrap/Col"; +import Tooltip from "@mui/material/Tooltip"; +import ArchiveDrawerFillIcon from "remixicon-react/ArchiveDrawerFillIcon"; function ItemStatComponent(props) { const [loaded, setLoaded] = useState(false); const handleImageLoad = () => { setLoaded(true); - } + }; - const backgroundImage=`/proxy/Items/Images/Backdrop?id=${props.data[0].Id}&fillWidth=300&quality=10`; + const backgroundImage = `/proxy/Items/Images/Backdrop?id=${props.data[0].Id}&fillWidth=300&quality=10`; const cardStyle = { backgroundImage: `url(${backgroundImage}), linear-gradient(to right, #00A4DC, #AA5CC3)`, - height:'100%', - backgroundSize: 'cover', + height: "100%", + backgroundSize: "cover", }; const cardBgStyle = { - backdropFilter: props.base_url ? 'blur(5px)' : 'blur(0px)', - backgroundColor: 'rgb(0, 0, 0, 0.6)', - height:'100%', + backdropFilter: props.base_url ? "blur(5px)" : "blur(0px)", + backgroundColor: "rgb(0, 0, 0, 0.6)", + height: "100%", }; - if (props.data.length === 0) { return <>; } - return (
- {props.icon ? -
- {props.icon} -
- : + {props.icon ? ( +
{props.icon}
+ ) : ( <> - {!props.data[0].archived && props.data && props.data[0] && props.data[0].PrimaryImageHash && props.data[0].PrimaryImageHash!=null && !loaded && ( -
- -
- )} - {!props.data[0].archived ? - + +
)} - src={"proxy/Items/Images/Primary?id=" + props.data[0].Id + "&fillWidth=400&quality=90"} - style={{ display: loaded ? 'block' : 'none' }} - onLoad={handleImageLoad} - onError={() => setLoaded(false)} - /> - : - -
- {props.data && props.data[0] && props.data[0].PrimaryImageHash && props.data[0].PrimaryImageHash!=null && ( - - - - )} -
- - Archived + {!props.data[0].archived ? ( + setLoaded(false)} + /> + ) : ( +
+ {props.data && props.data[0] && props.data[0].PrimaryImageHash && props.data[0].PrimaryImageHash != null && ( + + )} +
+ + Archived +
-
- - } + )} - } - + )} - - - + + +
{props.heading}
@@ -93,51 +91,43 @@ function ItemStatComponent(props) {
{props.data && - props.data.slice(0, 5).map((item, index) => ( -
- -
- {index + 1} - {item.UserId ? - - - {item.Name} - - - - : - (!item.Client && !props.icon) ? - - - - {item.Name} - - - : - (!item.Client && props.icon) ? - - - {item.Name} + props.data.slice(0, 5).map((item, index) => ( +
+
+ {index + 1} + {item.UserId ? ( + + + {item.Name} - : - + ) : !item.Client && !props.icon ? ( + + + {item.Name} + + + ) : !item.Client && props.icon ? ( + + + {item.Name} + + + ) : ( + {item.Client} - - } + + )} +
+ + {item.Plays || item.unique_viewers || item.Count}
- - - {item.Plays || item.unique_viewers} - - -
- ))} + ))} -
-
+ + ); } diff --git a/src/pages/components/statCards/playback_method_stats.jsx b/src/pages/components/statCards/playback_method_stats.jsx new file mode 100644 index 0000000..485443a --- /dev/null +++ b/src/pages/components/statCards/playback_method_stats.jsx @@ -0,0 +1,91 @@ +import { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ItemStatComponent from "./ItemStatComponent"; +import { Trans } from "react-i18next"; + +import BarChartGroupedLineIcon from "remixicon-react/BarChartGroupedLineIcon"; + +function PlaybackMethodStats(props) { + const translations = { + DirectPlay: , + Transocde: , + }; + const chartIcon = ; + + const [data, setData] = useState(); + const [days, setDays] = useState(30); + + const [config, setConfig] = useState(null); + + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config.getConfig(); + setConfig(newConfig); + } catch (error) { + if (error.code === "ERR_NETWORK") { + console.log(error); + } + } + }; + + const fetchStats = () => { + if (config) { + const url = `/stats/getPlaybackMethodStats`; + + axios + .post( + url, + { days: props.days }, + { + headers: { + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", + }, + } + ) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + if (!config) { + fetchConfig(); + } + + if (!data) { + fetchStats(); + } + if (days !== props.days) { + setDays(props.days); + fetchStats(); + } + + const intervalId = setInterval(fetchStats, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, config, days, props.days]); + + if (!data || data.length === 0) { + return <>; + } + + return ( + + stream.Name == "DirectPlay" ? { ...stream, Name: translations.DirectPlay } : { ...stream, Name: translations.Transocde } + )} + icon={chartIcon} + heading={} + units={} + /> + ); +} + +export default PlaybackMethodStats; diff --git a/src/pages/components/statistics/play-method-chart.jsx b/src/pages/components/statistics/play-method-chart.jsx new file mode 100644 index 0000000..8d5bbac --- /dev/null +++ b/src/pages/components/statistics/play-method-chart.jsx @@ -0,0 +1,91 @@ +import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, Legend } from "recharts"; + +function PlayMethodChart({ stats, types }) { + console.log(stats); + console.log(types); + const colors = [ + "rgb(54, 162, 235)", // blue + "rgb(255, 99, 132)", // pink + "rgb(75, 192, 192)", // teal + "rgb(255, 159, 64)", // orange + "rgb(153, 102, 255)", // lavender + "rgb(255, 205, 86)", // yellow + "rgb(201, 203, 207)", // light grey + "rgb(101, 119, 134)", // blue-grey + "rgb(255, 87, 87)", // light red + "rgb(50, 205, 50)", // lime green + "rgb(0, 255, 255)", // light cyan + "rgb(255, 255, 0)", // light yellow + "rgb(30, 144, 255)", // dodger blue + "rgb(192, 192, 192)", // silver + "rgb(255, 20, 147)", // deep pink + "rgb(105, 105, 105)", // dim grey + "rgb(240, 248, 255)", // alice blue + "rgb(255, 182, 193)", // light pink + "rgb(245, 222, 179)", // wheat + "rgb(147, 112, 219)", // medium purple + ]; + + const CustomTooltip = ({ payload, label, active }) => { + if (active) { + return ( +
+

{label}

+ {types.map((type, index) => ( +

{`${type.Name} : ${payload[index].value} Views`}

+ ))} +
+ ); + } + + return null; + }; + + const getMaxValue = () => { + let max = 0; + if (stats) { + stats.forEach((datum) => { + Object.keys(datum).forEach((key) => { + if (key !== "Key") { + max = Math.max(max, parseInt(datum[key])); + } + }); + }); + } + + return max; + }; + + const max = getMaxValue() + 10; + + return ( + + + + {types.map((type, index) => ( + + + + + ))} + + + + } /> + + {types.map((type, index) => ( + + ))} + + + ); +} + +export default PlayMethodChart; diff --git a/src/pages/components/statistics/playbackMethodStats.jsx b/src/pages/components/statistics/playbackMethodStats.jsx new file mode 100644 index 0000000..e7b9a91 --- /dev/null +++ b/src/pages/components/statistics/playbackMethodStats.jsx @@ -0,0 +1,84 @@ +import { useState, useEffect } from "react"; +import axios from "axios"; + +import "../../css/stats.css"; +import { Trans } from "react-i18next"; +import PlayMethodChart from "./play-method-chart"; + +function PlayMethodStats(props) { + const [stats, setStats] = useState(); + const [types, setTypes] = useState(); + const [hours, setHours] = useState(999); + const token = localStorage.getItem("token"); + + useEffect(() => { + const fetchLibraries = () => { + const url = `/stats/getLibraryItemsPlayMethodStats`; + + axios + .post( + url, + { + hours: 999, + libraryid: props.libraryid ?? "a656b907eb3a73532e40e44b968d0225", + }, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ) + .then((data) => { + setStats(data.data.stats); + setTypes(data.data.types); + }) + .catch((error) => { + console.log(error); + }); + }; + + if (!stats) { + fetchLibraries(); + } + if (hours !== props.hours) { + setHours(props.hours); + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [hours, props.hours, token]); + + if (!stats) { + return <>; + } + + if (stats.length === 0) { + return ( +
+

+ - {hours} 1 ? "S" : ""}`} /> +

+ +

+ +

+
+ ); + } + return ( +
+

+ - {hours}{" "} + 1 ? "S" : ""}`} /> +

+ +
+ +
+
+ ); +} + +export default PlayMethodStats; diff --git a/src/pages/testing.jsx b/src/pages/testing.jsx index 6711203..f3c8fb3 100644 --- a/src/pages/testing.jsx +++ b/src/pages/testing.jsx @@ -1,10 +1,14 @@ import { Routes, Route } from "react-router-dom"; import Sessions from "./debugTools/sessions"; +import PlayMethodStats from "./components/statistics/playbackMethodStats"; +import PlaybackMethodStats from "./components/statCards/playback_method_stats"; const TestingRoutes = () => { return ( } /> + } /> + } /> ); };