feat: Added Playback concurrent stream card

fixed translation key typo in stream-info modal
This commit is contained in:
CyferShepard
2024-08-02 14:58:49 +02:00
parent 0919a7c3de
commit cf0e85d934
9 changed files with 591 additions and 251 deletions

View File

@@ -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;

View File

@@ -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",

View File

@@ -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 (
<div className="watch-stats">
<div className="Heading my-3">
<h1><Trans i18nKey="HOME_PAGE.WATCH_STATISTIC" /></h1>
<h1>
<Trans i18nKey="HOME_PAGE.WATCH_STATISTIC" />
</h1>
<div className="date-range">
<div className="header"><Trans i18nKey="LAST" /></div>
<div className="header">
<Trans i18nKey="LAST" />
</div>
<div className="days">
<input
type="number"
@@ -44,24 +49,23 @@ function HomeStatisticCards() {
onKeyDown={handleKeyDown}
/>
</div>
<div className="trailer"><Trans i18nKey="UNITS.DAYS" /></div>
<div className="trailer">
<Trans i18nKey="UNITS.DAYS" />
</div>
</div>
</div>
<div className="grid-stat-cards">
<MVMovies days={days} />
<MPMovies days={days} />
<MVSeries days={days} />
<MPSeries days={days} />
<MVMusic days={days}/>
<MPMusic days={days}/>
<MVMusic days={days} />
<MPMusic days={days} />
<MVLibraries days={days} />
<MostUsedClient days={days} />
<MostActiveUsers days={days} />
</div>
<PlaybackMethodStats days={days} />
</div>
</div>
);
}

View File

@@ -101,7 +101,7 @@ function Row(logs) {
</TableRow>
<TableRow>
<TableCell className="py-0 pb-1" ><Trans i18nKey={"APECT_RATIO"}/></TableCell>
<TableCell className="py-0 pb-1" ><Trans i18nKey={"ASPECT_RATIO"}/></TableCell>
<TableCell className="py-0 pb-1" >{data.MediaStreams ? data.MediaStreams.find(stream => stream.Type === 'Video')?.AspectRatio : '-'}</TableCell>
<TableCell className="py-0 pb-1" >{data.MediaStreams ? data.MediaStreams.find(stream => stream.Type === 'Video')?.AspectRatio : '-'}</TableCell>
</TableRow>

View File

@@ -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 (
<Card className="stat-card rounded-2" style={cardStyle}>
<div style={cardBgStyle} className="rounded-2">
<Row className="h-100 rounded-2">
<Col className="d-none d-lg-block stat-card-banner">
{props.icon ?
<div className="stat-card-icon">
{props.icon}
</div>
:
{props.icon ? (
<div className="stat-card-icon">{props.icon}</div>
) : (
<>
{!props.data[0].archived && props.data && props.data[0] && props.data[0].PrimaryImageHash && props.data[0].PrimaryImageHash!=null && !loaded && (
<div className="position-absolute w-100 h-100">
<Blurhash hash={props.data[0].PrimaryImageHash} height={'100%'} className="rounded-3 overflow-hidden"/>
</div>
)}
{!props.data[0].archived ?
<Card.Img
className={props.isAudio ? (
"stat-card-image-audio rounded-0 rounded-start"
) : (
"stat-card-image rounded-0 rounded-start"
{!props.data[0].archived &&
props.data &&
props.data[0] &&
props.data[0].PrimaryImageHash &&
props.data[0].PrimaryImageHash != null &&
!loaded && (
<div className="position-absolute w-100 h-100">
<Blurhash hash={props.data[0].PrimaryImageHash} height={"100%"} className="rounded-3 overflow-hidden" />
</div>
)}
src={"proxy/Items/Images/Primary?id=" + props.data[0].Id + "&fillWidth=400&quality=90"}
style={{ display: loaded ? 'block' : 'none' }}
onLoad={handleImageLoad}
onError={() => setLoaded(false)}
/>
:
<div>
{props.data && props.data[0] && props.data[0].PrimaryImageHash && props.data[0].PrimaryImageHash!=null && (
<Blurhash hash={props.data[0].PrimaryImageHash} height={'180px'} className="rounded-3 overflow-hidden position-absolute"/>
)}
<div className="d-flex flex-column justify-content-center align-items-center stat-card-image position-absolute">
<ArchiveDrawerFillIcon className="w-100 h-100"/>
<span>Archived</span>
{!props.data[0].archived ? (
<Card.Img
className={
props.isAudio ? "stat-card-image-audio rounded-0 rounded-start" : "stat-card-image rounded-0 rounded-start"
}
src={"proxy/Items/Images/Primary?id=" + props.data[0].Id + "&fillWidth=400&quality=90"}
style={{ display: loaded ? "block" : "none" }}
onLoad={handleImageLoad}
onError={() => setLoaded(false)}
/>
) : (
<div>
{props.data && props.data[0] && props.data[0].PrimaryImageHash && props.data[0].PrimaryImageHash != null && (
<Blurhash
hash={props.data[0].PrimaryImageHash}
height={"180px"}
className="rounded-3 overflow-hidden position-absolute"
/>
)}
<div className="d-flex flex-column justify-content-center align-items-center stat-card-image position-absolute">
<ArchiveDrawerFillIcon className="w-100 h-100" />
<span>Archived</span>
</div>
</div>
</div>
}
)}
</>
}
)}
</Col>
<Col className="w-100">
<Card.Body className="w-100" >
<Card.Header className="d-flex justify-content-between border-0 p-0 bg-transparent">
<Col className="w-100">
<Card.Body className="w-100">
<Card.Header className="d-flex justify-content-between border-0 p-0 bg-transparent">
<div>
<Card.Subtitle className="stat-items">{props.heading}</Card.Subtitle>
</div>
@@ -93,51 +91,43 @@ function ItemStatComponent(props) {
</div>
</Card.Header>
{props.data &&
props.data.slice(0, 5).map((item, index) => (
<div className="d-flex justify-content-between stat-items" key={item.Id || index}>
<div className="d-flex justify-content-between" key={item.Id || index}>
<Card.Text className="stat-item-index m-0">{index + 1}</Card.Text>
{item.UserId ?
<Link to={`/users/${item.UserId}`} className="item-name">
<Tooltip title={item.Name} >
<Card.Text>{item.Name}</Card.Text>
</Tooltip>
</Link>
:
(!item.Client && !props.icon) ?
<Link to={`/libraries/item/${item.Id}`} className="item-name">
<Tooltip title={item.Name} >
<Card.Text>{item.Name}</Card.Text>
</Tooltip>
</Link>
:
(!item.Client && props.icon) ?
<Link to={`/libraries/${item.Id}`} className="item-name">
<Tooltip title={item.Name} >
<Card.Text>{item.Name}</Card.Text>
props.data.slice(0, 5).map((item, index) => (
<div className="d-flex justify-content-between stat-items" key={item.Id || index}>
<div className="d-flex justify-content-between" key={item.Id || index}>
<Card.Text className="stat-item-index m-0">{index + 1}</Card.Text>
{item.UserId ? (
<Link to={`/users/${item.UserId}`} className="item-name">
<Tooltip title={item.Name}>
<Card.Text>{item.Name}</Card.Text>
</Tooltip>
</Link>
:
<Tooltip title={item.Client} >
) : !item.Client && !props.icon ? (
<Link to={`/libraries/item/${item.Id}`} className="item-name">
<Tooltip title={item.Name}>
<Card.Text>{item.Name}</Card.Text>
</Tooltip>
</Link>
) : !item.Client && props.icon ? (
<Link to={`/libraries/${item.Id}`} className="item-name">
<Tooltip title={item.Name}>
<Card.Text>{item.Name}</Card.Text>
</Tooltip>
</Link>
) : (
<Tooltip title={item.Client}>
<Card.Text>{item.Client}</Card.Text>
</Tooltip>
}
</Tooltip>
)}
</div>
<Card.Text className="stat-item-count">{item.Plays || item.unique_viewers || item.Count}</Card.Text>
</div>
<Card.Text className="stat-item-count">
{item.Plays || item.unique_viewers}
</Card.Text>
</div>
))}
))}
</Card.Body>
</Col>
</Row>
</div>
</Card>
</div>
</Card>
);
}

View File

@@ -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: <Trans i18nKey="DIRECT" />,
Transocde: <Trans i18nKey="TRANSCODE" />,
};
const chartIcon = <BarChartGroupedLineIcon size={"100%"} />;
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 (
<ItemStatComponent
base_url={config.hostUrl}
data={data.map((stream) =>
stream.Name == "DirectPlay" ? { ...stream, Name: translations.DirectPlay } : { ...stream, Name: translations.Transocde }
)}
icon={chartIcon}
heading={<Trans i18nKey="STAT_CARDS.CONCURRENT_STREAMS" />}
units={<Trans i18nKey="UNITS.STREAMS" />}
/>
);
}
export default PlaybackMethodStats;

View File

@@ -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 (
<div style={{ backgroundColor: "rgba(0,0,0,0.8)", color: "white" }} className="p-2 rounded-2 border-0">
<p className="text-center fs-5">{label}</p>
{types.map((type, index) => (
<p key={type.Id} style={{ color: `${colors[index]}` }}>{`${type.Name} : ${payload[index].value} Views`}</p>
))}
</div>
);
}
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 (
<ResponsiveContainer width="100%">
<AreaChart data={stats} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
{types.map((type, index) => (
<linearGradient key={type.Id} id={type.Name} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={colors[index]} stopOpacity={0.8} />
<stop offset="95%" stopColor={colors[index]} stopOpacity={0} />
</linearGradient>
))}
</defs>
<XAxis dataKey="Key" interval={0} angle={-60} textAnchor="end" height={100} />
<YAxis domain={[0, max]} />
<Tooltip content={<CustomTooltip />} />
<Legend verticalAlign="bottom" />
{types.map((type, index) => (
<Area
key={type.Id}
type="monotone"
dataKey={type.Name}
stroke={colors[index]}
fillOpacity={1}
fill={"url(#" + type.Name + ")"}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
}
export default PlayMethodChart;

View File

@@ -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 (
<div className="main-widget">
<h1>
<Trans i18nKey={"STAT_PAGE.DAILY_PLAY_PER_LIBRARY"} /> - {hours} <Trans i18nKey={`UNITS.HOUR${hours > 1 ? "S" : ""}`} />
</h1>
<h1>
<Trans i18nKey={"ERROR_MESSAGES.NO_STATS"} />
</h1>
</div>
);
}
return (
<div className="main-widget">
<h2 className="text-start my-2">
<Trans i18nKey={"STAT_PAGE.DAILY_PLAY_PER_LIBRARY"} /> - <Trans i18nKey={"LAST"} /> {hours}{" "}
<Trans i18nKey={`UNITS.HOUR${hours > 1 ? "S" : ""}`} />
</h2>
<div className="graph">
<PlayMethodChart types={types} stats={stats} />
</div>
</div>
);
}
export default PlayMethodStats;

View File

@@ -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 (
<Routes>
<Route path="/sessions" element={<Sessions />} />
<Route path="/stats" element={<PlayMethodStats />} />
<Route path="/statsstream" element={<PlaybackMethodStats />} />
</Routes>
);
};