mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
feat: Added Playback concurrent stream card
fixed translation key typo in stream-info modal
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
91
src/pages/components/statCards/playback_method_stats.jsx
Normal file
91
src/pages/components/statCards/playback_method_stats.jsx
Normal 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;
|
||||
91
src/pages/components/statistics/play-method-chart.jsx
Normal file
91
src/pages/components/statistics/play-method-chart.jsx
Normal 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;
|
||||
84
src/pages/components/statistics/playbackMethodStats.jsx
Normal file
84
src/pages/components/statistics/playbackMethodStats.jsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user