mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-19 00:37:22 +01:00
Libraries Page
Basic Libraries and Libraries Info page completed Auto init still broken Need to remove redundant components
This commit is contained in:
@@ -39,7 +39,6 @@ router.post("/setconfig", async (req, res) => {
|
||||
query,
|
||||
[JF_HOST, JF_API_KEY]
|
||||
);
|
||||
console.log({ JF_HOST: JF_HOST, JF_API_KEY: JF_API_KEY });
|
||||
res.send(rows);
|
||||
}catch(error)
|
||||
{
|
||||
|
||||
167
backend/init.sql
167
backend/init.sql
@@ -5,7 +5,7 @@
|
||||
-- Dumped from database version 15.2 (Debian 15.2-1.pgdg110+1)
|
||||
-- Dumped by pg_dump version 15.1
|
||||
|
||||
-- Started on 2023-03-25 19:59:41 UTC
|
||||
-- Started on 2023-03-26 14:21:37 UTC
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
@@ -19,7 +19,45 @@ SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- TOC entry 245 (class 1255 OID 49383)
|
||||
-- TOC entry 251 (class 1255 OID 49412)
|
||||
-- Name: fs_last_library_activity(text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_last_library_activity(libraryid text) RETURNS TABLE("Id" text, "Name" text, "EpisodeName" text, "SeasonNumber" integer, "EpisodeNumber" integer, "PrimaryImageHash" text, "UserId" text, "UserName" text, "LastPlayed" interval)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT DISTINCT ON (i."Name", e."Name")
|
||||
i."Id",
|
||||
i."Name",
|
||||
e."Name" AS "EpisodeName",
|
||||
CASE WHEN a."SeasonId" IS NOT NULL THEN s."IndexNumber" ELSE NULL END AS "SeasonNumber",
|
||||
CASE WHEN a."SeasonId" IS NOT NULL THEN e."IndexNumber" ELSE NULL END AS "EpisodeNumber",
|
||||
i."PrimaryImageHash",
|
||||
a."UserId",
|
||||
a."UserName",
|
||||
(NOW() - a."ActivityDateInserted") as "LastPlayed"
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
||||
JOIN jf_libraries l ON i."ParentId" = l."Id"
|
||||
LEFT JOIN jf_library_seasons s ON s."Id" = a."SeasonId"
|
||||
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = a."EpisodeId"
|
||||
WHERE l."Id" = libraryid
|
||||
ORDER BY i."Name", e."Name", a."ActivityDateInserted" DESC
|
||||
) AS latest_distinct_rows
|
||||
ORDER BY "LastPlayed"
|
||||
LIMIT 15;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_last_library_activity(libraryid text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 247 (class 1255 OID 49383)
|
||||
-- Name: fs_last_user_activity(text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -55,7 +93,36 @@ $$;
|
||||
ALTER FUNCTION public.fs_last_user_activity(userid text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 232 (class 1255 OID 41783)
|
||||
-- TOC entry 245 (class 1255 OID 49411)
|
||||
-- Name: fs_library_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_library_stats(hours integer, libraryid text) RETURNS TABLE("Plays" bigint, total_playback_duration numeric, "Id" text, "Name" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT count(*) AS "Plays",
|
||||
sum(a."PlaybackDuration") AS total_playback_duration,
|
||||
l."Id",
|
||||
l."Name"
|
||||
FROM jf_playback_activity a
|
||||
join jf_library_items i
|
||||
on a."NowPlayingItemId"=i."Id"
|
||||
join jf_libraries l
|
||||
on i."ParentId"=l."Id"
|
||||
WHERE a."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(hours => hours) AND NOW()
|
||||
and l."Id"=libraryid
|
||||
GROUP BY l."Id", l."Name"
|
||||
ORDER BY (count(*)) DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_library_stats(hours integer, libraryid text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 233 (class 1255 OID 41783)
|
||||
-- Name: fs_most_active_user(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -78,7 +145,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_active_user(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 247 (class 1255 OID 49386)
|
||||
-- TOC entry 249 (class 1255 OID 49386)
|
||||
-- Name: fs_most_played_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -119,7 +186,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_played_items(days integer, itemtype text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 248 (class 1255 OID 49394)
|
||||
-- TOC entry 250 (class 1255 OID 49394)
|
||||
-- Name: fs_most_popular_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -167,7 +234,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_popular_items(days integer, itemtype text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 231 (class 1255 OID 41730)
|
||||
-- TOC entry 232 (class 1255 OID 41730)
|
||||
-- Name: fs_most_used_clients(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -189,7 +256,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_used_clients(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 246 (class 1255 OID 49385)
|
||||
-- TOC entry 248 (class 1255 OID 49385)
|
||||
-- Name: fs_most_viewed_libraries(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -233,7 +300,7 @@ $$;
|
||||
ALTER FUNCTION public.fs_most_viewed_libraries(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 244 (class 1255 OID 49364)
|
||||
-- TOC entry 246 (class 1255 OID 49364)
|
||||
-- Name: fs_user_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -533,7 +600,69 @@ CREATE TABLE public.jf_library_seasons (
|
||||
ALTER TABLE public.jf_library_seasons OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 3230 (class 2606 OID 16401)
|
||||
-- TOC entry 231 (class 1259 OID 49405)
|
||||
-- Name: js_library_stats_overview; Type: VIEW; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE VIEW public.js_library_stats_overview AS
|
||||
SELECT DISTINCT ON (l."Id") l."Id",
|
||||
l."Name",
|
||||
l."ServerId",
|
||||
l."IsFolder",
|
||||
l."Type",
|
||||
l."CollectionType",
|
||||
l."ImageTagsPrimary",
|
||||
i."Id" AS "ItemId",
|
||||
i."Name" AS "ItemName",
|
||||
i."Type" AS "ItemType",
|
||||
i."PrimaryImageHash",
|
||||
s."IndexNumber" AS "SeasonNumber",
|
||||
e."IndexNumber" AS "EpisodeNumber",
|
||||
e."Name" AS "EpisodeName",
|
||||
( SELECT count(*) AS count
|
||||
FROM (public.jf_playback_activity a
|
||||
JOIN public.jf_library_items i_1 ON ((a."NowPlayingItemId" = i_1."Id")))
|
||||
WHERE (i_1."ParentId" = l."Id")) AS "Plays",
|
||||
( SELECT sum(a."PlaybackDuration") AS sum
|
||||
FROM (public.jf_playback_activity a
|
||||
JOIN public.jf_library_items i_1 ON ((a."NowPlayingItemId" = i_1."Id")))
|
||||
WHERE (i_1."ParentId" = l."Id")) AS total_playback_duration,
|
||||
cv."Library_Count",
|
||||
cv."Season_Count",
|
||||
cv."Episode_Count",
|
||||
(now() - latest_activity."ActivityDateInserted") AS "LastActivity"
|
||||
FROM (((((public.jf_libraries l
|
||||
JOIN public.jf_library_count_view cv ON ((cv."Id" = l."Id")))
|
||||
LEFT JOIN ( SELECT jf_playback_activity."Id",
|
||||
jf_playback_activity."IsPaused",
|
||||
jf_playback_activity."UserId",
|
||||
jf_playback_activity."UserName",
|
||||
jf_playback_activity."Client",
|
||||
jf_playback_activity."DeviceName",
|
||||
jf_playback_activity."DeviceId",
|
||||
jf_playback_activity."ApplicationVersion",
|
||||
jf_playback_activity."NowPlayingItemId",
|
||||
jf_playback_activity."NowPlayingItemName",
|
||||
jf_playback_activity."SeasonId",
|
||||
jf_playback_activity."SeriesName",
|
||||
jf_playback_activity."EpisodeId",
|
||||
jf_playback_activity."PlaybackDuration",
|
||||
jf_playback_activity."ActivityDateInserted",
|
||||
jf_playback_activity."PlayMethod",
|
||||
i_1."ParentId"
|
||||
FROM (public.jf_playback_activity
|
||||
JOIN public.jf_library_items i_1 ON ((i_1."Id" = jf_playback_activity."NowPlayingItemId")))
|
||||
ORDER BY jf_playback_activity."ActivityDateInserted" DESC) latest_activity ON ((l."Id" = latest_activity."ParentId")))
|
||||
LEFT JOIN public.jf_library_items i ON ((i."Id" = latest_activity."NowPlayingItemId")))
|
||||
LEFT JOIN public.jf_library_seasons s ON ((s."Id" = latest_activity."SeasonId")))
|
||||
LEFT JOIN public.jf_library_episodes e ON ((e."EpisodeId" = latest_activity."EpisodeId")))
|
||||
ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
|
||||
|
||||
|
||||
ALTER TABLE public.js_library_stats_overview OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 3236 (class 2606 OID 16401)
|
||||
-- Name: app_config app_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -542,7 +671,7 @@ ALTER TABLE ONLY public.app_config
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3232 (class 2606 OID 16419)
|
||||
-- TOC entry 3238 (class 2606 OID 16419)
|
||||
-- Name: jf_libraries jf_libraries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -551,7 +680,7 @@ ALTER TABLE ONLY public.jf_libraries
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3238 (class 2606 OID 24912)
|
||||
-- TOC entry 3244 (class 2606 OID 24912)
|
||||
-- Name: jf_library_episodes jf_library_episodes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -560,7 +689,7 @@ ALTER TABLE ONLY public.jf_library_episodes
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3234 (class 2606 OID 24605)
|
||||
-- TOC entry 3240 (class 2606 OID 24605)
|
||||
-- Name: jf_library_items jf_library_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -569,7 +698,7 @@ ALTER TABLE ONLY public.jf_library_items
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3236 (class 2606 OID 24737)
|
||||
-- TOC entry 3242 (class 2606 OID 24737)
|
||||
-- Name: jf_library_seasons jf_library_seasons_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -578,7 +707,7 @@ ALTER TABLE ONLY public.jf_library_seasons
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3240 (class 2606 OID 41737)
|
||||
-- TOC entry 3246 (class 2606 OID 41737)
|
||||
-- Name: jf_users jf_users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -587,7 +716,7 @@ ALTER TABLE ONLY public.jf_users
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3384 (class 2618 OID 25163)
|
||||
-- TOC entry 3390 (class 2618 OID 25163)
|
||||
-- Name: jf_library_count_view _RETURN; Type: RULE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -607,7 +736,7 @@ CREATE OR REPLACE VIEW public.jf_library_count_view AS
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3241 (class 2606 OID 24617)
|
||||
-- TOC entry 3247 (class 2606 OID 24617)
|
||||
-- Name: jf_library_items jf_library_items_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
@@ -616,15 +745,15 @@ ALTER TABLE ONLY public.jf_library_items
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3391 (class 0 OID 0)
|
||||
-- Dependencies: 3241
|
||||
-- TOC entry 3398 (class 0 OID 0)
|
||||
-- Dependencies: 3247
|
||||
-- Name: CONSTRAINT jf_library_items_fkey ON jf_library_items; Type: COMMENT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items IS 'jf_library';
|
||||
|
||||
|
||||
-- Completed on 2023-03-25 19:59:42 UTC
|
||||
-- Completed on 2023-03-26 14:21:38 UTC
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
|
||||
@@ -226,5 +226,60 @@ router.post("/getUserLastPlayed", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getLibraryDetails", async (req, res) => {
|
||||
try {
|
||||
const { libraryid } = req.body;
|
||||
const { rows } = await db.query(
|
||||
`select * from jf_libraries where "Id"='${libraryid}'`
|
||||
);
|
||||
res.send(rows[0]);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getGlobalLibraryStats", async (req, res) => {
|
||||
try {
|
||||
const { hours,libraryid } = req.body;
|
||||
let _hours = hours;
|
||||
if (hours === undefined) {
|
||||
_hours = 24;
|
||||
}
|
||||
console.log(`select * from fs_library_stats(${_hours},'${libraryid}')`);
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_library_stats(${_hours},'${libraryid}')`
|
||||
);
|
||||
res.send(rows[0]);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get("/getLibraryStats", async (req, res) => {
|
||||
try {
|
||||
const { rows } = await db.query("select * from js_library_stats_overview");
|
||||
res.send(rows);
|
||||
} catch (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('${libraryid}') limit 15`
|
||||
);
|
||||
res.send(rows);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -21,7 +21,6 @@ async function ActivityMonitor(interval) {
|
||||
const apiKey = config[0].JF_API_KEY;
|
||||
|
||||
if (base_url === null || config[0].JF_API_KEY === null) {
|
||||
console.log("Config Details Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import Settings from './pages/settings';
|
||||
import Users from './pages/users';
|
||||
import UserInfo from './pages/components/user-info';
|
||||
import Libraries from './pages/libraries';
|
||||
import LibraryInfo from './pages/components/library-info';
|
||||
import ErrorPage from './pages/components/general/error';
|
||||
|
||||
|
||||
@@ -79,6 +80,7 @@ if (!config || config.apiKey ==null) {
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/users/:UserId" element={<UserInfo />} />
|
||||
<Route path="/libraries" element={<Libraries />} />
|
||||
<Route path="/libraries/:LibraryId" element={<LibraryInfo />} />
|
||||
<Route path="/testing" element={<Testing />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -11,6 +11,7 @@ body {
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: white ;
|
||||
}
|
||||
|
||||
code {
|
||||
|
||||
22
src/pages/components/library-info.js
Normal file
22
src/pages/components/library-info.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import LibraryDetails from './library/library-details';
|
||||
import LibraryGlobalStats from './library/library-stats';
|
||||
import LastLibraryPlayed from './library/lastplayed';
|
||||
|
||||
|
||||
|
||||
|
||||
function LibraryInfo() {
|
||||
const { LibraryId } = useParams();
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LibraryDetails LibraryId={LibraryId}/>
|
||||
<LibraryGlobalStats LibraryId={LibraryId}/>
|
||||
<LastLibraryPlayed LibraryId={LibraryId}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default LibraryInfo;
|
||||
65
src/pages/components/library/globalstats/watchtimestats.js
Normal file
65
src/pages/components/library/globalstats/watchtimestats.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
|
||||
import "../../../css/globalstats.css";
|
||||
|
||||
function WatchTimeStats(props) {
|
||||
|
||||
function formatTime(totalSeconds, numberClassName, labelClassName) {
|
||||
const units = [
|
||||
{ label: 'Day', seconds: 86400 },
|
||||
{ label: 'Hour', seconds: 3600 },
|
||||
{ label: 'Minute', seconds: 60 },
|
||||
];
|
||||
|
||||
const parts = units.reduce((result, { label, seconds }) => {
|
||||
const value = Math.floor(totalSeconds / seconds);
|
||||
if (value) {
|
||||
const formattedValue = <p className={numberClassName}>{value}</p>;
|
||||
const formattedLabel = (
|
||||
<span className={labelClassName}>
|
||||
{label}
|
||||
{value === 1 ? '' : 's'}
|
||||
</span>
|
||||
);
|
||||
result.push(
|
||||
<span key={label} className="time-part">
|
||||
{formattedValue} {formattedLabel}
|
||||
</span>
|
||||
);
|
||||
totalSeconds -= value * seconds;
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<p className={numberClassName}>0</p>{' '}
|
||||
<p className={labelClassName}>Minutes</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="global-stats">
|
||||
<div className="stats-header">
|
||||
<div>{props.heading}</div>
|
||||
</div>
|
||||
|
||||
<div className="play-duration-stats" key={props.data.UserId}>
|
||||
<p className="stat-value"> {props.data.Plays || 0}</p>
|
||||
<p className="stat-unit" >Plays /</p>
|
||||
|
||||
<>{formatTime(props.data.total_playback_duration || 0,'stat-value','stat-unit')}</>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WatchTimeStats;
|
||||
68
src/pages/components/library/lastplayed.js
Normal file
68
src/pages/components/library/lastplayed.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import LastPlayedItem from "./lastplayed/last-played-item";
|
||||
|
||||
import Config from "../../../lib/config";
|
||||
import "../../css/users/user-details.css";
|
||||
|
||||
function LastLibraryPlayed(props) {
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const itemData = await axios.post(`/stats/getLibraryLastPlayed`, {
|
||||
libraryid: props.LibraryId,
|
||||
});
|
||||
setData(itemData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data,config, props.LibraryId]);
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (!data || !config) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="last-played">
|
||||
<h1>Last Watched</h1>
|
||||
<div className="last-played-container">
|
||||
{data.map((item) => (
|
||||
<LastPlayedItem data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LastLibraryPlayed;
|
||||
66
src/pages/components/library/lastplayed/last-played-item.js
Normal file
66
src/pages/components/library/lastplayed/last-played-item.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, {useState} from "react";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
|
||||
function formatTime(time) {
|
||||
|
||||
const units = {
|
||||
days: ['Day', 'Days'],
|
||||
hours: ['Hour', 'Hours'],
|
||||
minutes: ['Minute', 'Minutes'],
|
||||
seconds: ['Second', 'Seconds']
|
||||
};
|
||||
|
||||
let formattedTime = '';
|
||||
|
||||
if (time.days) {
|
||||
formattedTime = `${time.days} ${units.days[time.days > 1 ? 1 : 0]}`;
|
||||
} else if (time.hours) {
|
||||
formattedTime = `${time.hours} ${units.hours[time.hours > 1 ? 1 : 0]}`;
|
||||
} else if (time.minutes) {
|
||||
formattedTime = `${time.minutes} ${units.minutes[time.minutes > 1 ? 1 : 0]}`;
|
||||
} else {
|
||||
formattedTime = `${time.seconds} ${units.seconds[time.seconds > 1 ? 1 : 0]}`;
|
||||
}
|
||||
|
||||
return `${formattedTime} ago`;
|
||||
}
|
||||
|
||||
|
||||
function LastPlayedItem(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="last-card">
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : <Blurhash hash={props.data.PrimaryImageHash} width={'100%'} height={'100%'}/>}
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
props.data.Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="last-item-details">
|
||||
<div className="last-last-played">
|
||||
{formatTime(props.data.LastPlayed)}
|
||||
</div>
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
<div className="last-item-episode"> {props.data.EpisodeName}</div>
|
||||
</div>
|
||||
{props.data.SeasonNumber ?
|
||||
<div className="last-item-episode number"> S{props.data.SeasonNumber} - E{props.data.EpisodeNumber}</div>:
|
||||
<></>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LastPlayedItem;
|
||||
@@ -1,22 +1,113 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import "../../css/library/library-card.css";
|
||||
|
||||
|
||||
|
||||
function LibraryCard(props) {
|
||||
return (
|
||||
<div
|
||||
className="library-card-banner"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
props.data.Id +
|
||||
"/Images/Primary/?fillWidth=300&quality=90"})`,
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
|
||||
function formatTotalWatchTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600); // 1 hour = 3600 seconds
|
||||
const minutes = Math.floor((seconds % 3600) / 60); // 1 minute = 60 seconds
|
||||
let formattedTime='';
|
||||
if(hours)
|
||||
{
|
||||
formattedTime+=`${hours} hours`;
|
||||
}
|
||||
if(minutes)
|
||||
{
|
||||
formattedTime+=` ${minutes} minutes`;
|
||||
}
|
||||
if(!hours && !minutes)
|
||||
{
|
||||
formattedTime=`0 minutes`;
|
||||
}
|
||||
|
||||
return formattedTime ;
|
||||
}
|
||||
|
||||
function formatLastActivityTime(time) {
|
||||
const units = {
|
||||
days: ['Day', 'Days'],
|
||||
hours: ['Hour', 'Hours'],
|
||||
minutes: ['Minute', 'Minutes']
|
||||
};
|
||||
|
||||
let formattedTime = '';
|
||||
|
||||
for (const unit in units) {
|
||||
if (time[unit]) {
|
||||
const unitName = units[unit][time[unit] > 1 ? 1 : 0];
|
||||
formattedTime += `${time[unit]} ${unitName} `;
|
||||
}
|
||||
}
|
||||
|
||||
return `${formattedTime}ago`;
|
||||
}
|
||||
return (
|
||||
<div className="library-card">
|
||||
|
||||
<Link to={`/libraries/${props.data.Id}`}>
|
||||
<div
|
||||
className="library-card-banner"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
props.data.Id +
|
||||
"/Images/Primary/?fillWidth=400&quality=90"
|
||||
})`,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="library-card-details">
|
||||
<div>
|
||||
<p className="label">Library</p>
|
||||
<p>{props.data.Name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="label">Type</p>
|
||||
<p>{props.data.CollectionType==='tvshows' ? 'Series' : "Movies"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="label">Total Plays</p>
|
||||
<p>{props.data.Plays}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="label">Total Playback</p>
|
||||
<p>{formatTotalWatchTime(props.data.total_playback_duration)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="label">Last Played</p>
|
||||
<p>{props.data.ItemName ? props.data.ItemName : 'n/a'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="label">Last Activity</p>
|
||||
<p>{props.data.LastActivity ? formatLastActivityTime(props.data.LastActivity) : 'never'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="label">{props.data.CollectionType==='tvshows' ? 'Series' : "Movies"}</p>
|
||||
<p>{props.data.Library_Count}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="label">Seasons</p>
|
||||
<p>{props.data.CollectionType==='tvshows' ? props.data.Season_Count : ''}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="label">Episodes</p>
|
||||
<p>{props.data.CollectionType==='tvshows' ? props.data.Episode_Count : ''}</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
52
src/pages/components/library/library-details.js
Normal file
52
src/pages/components/library/library-details.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
|
||||
// import "../../css/users/user-details.css";
|
||||
|
||||
function LibraryDetails(props) {
|
||||
const [data, setData] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const libraryrData = await axios.post(`/stats/getLibraryDetails`, {
|
||||
libraryid: props.LibraryId,
|
||||
});
|
||||
setData(libraryrData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, props.LibraryId]);
|
||||
|
||||
|
||||
if (!data) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="user-detail-container">
|
||||
<div className="user-image-container">
|
||||
{data.CollectionType==="tvshows" ?
|
||||
|
||||
<TvLineIcon size={'100%'}/>
|
||||
:
|
||||
<FilmLineIcon size={'100%'}/>
|
||||
}
|
||||
</div>
|
||||
<p className="user-name">{data.Name}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibraryDetails;
|
||||
62
src/pages/components/library/library-stats.js
Normal file
62
src/pages/components/library/library-stats.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import "../../css/globalstats.css";
|
||||
|
||||
import WatchTimeStats from "./globalstats/watchtimestats";
|
||||
|
||||
function LibraryGlobalStats(props) {
|
||||
const [dayStats, setDayStats] = useState({});
|
||||
const [weekStats, setWeekStats] = useState({});
|
||||
const [monthStats, setMonthStats] = useState({});
|
||||
const [allStats, setAllStats] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const dayData = await axios.post(`/stats/getGlobalLibraryStats`, {
|
||||
hours: (24*1),
|
||||
libraryid: props.LibraryId,
|
||||
});
|
||||
setDayStats(dayData.data);
|
||||
|
||||
const weekData = await axios.post(`/stats/getGlobalLibraryStats`, {
|
||||
hours: (24*7),
|
||||
libraryid: props.LibraryId,
|
||||
});
|
||||
setWeekStats(weekData.data);
|
||||
|
||||
const monthData = await axios.post(`/stats/getGlobalLibraryStats`, {
|
||||
hours: (24*30),
|
||||
libraryid: props.LibraryId,
|
||||
});
|
||||
setMonthStats(monthData.data);
|
||||
|
||||
const allData = await axios.post(`/stats/getGlobalLibraryStats`, {
|
||||
hours: (24*999),
|
||||
libraryid: props.LibraryId,
|
||||
});
|
||||
setAllStats(allData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [props.LibraryId]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Library Stats</h1>
|
||||
<div className="global-stats-container">
|
||||
<WatchTimeStats data={dayStats} heading={"Last 24 Hours"} />
|
||||
<WatchTimeStats data={weekStats} heading={"Last 7 Days"} />
|
||||
<WatchTimeStats data={monthStats} heading={"Last 30 Days"} />
|
||||
<WatchTimeStats data={allStats} heading={"All Time"} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibraryGlobalStats;
|
||||
@@ -7,7 +7,7 @@ function LibraryStatComponent(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="library-card">
|
||||
<div className="library-stat-card">
|
||||
|
||||
<div className="library-image">
|
||||
<div className="library-icons">
|
||||
|
||||
@@ -7,7 +7,7 @@ const TerminalComponent = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// create a new WebSocket connection
|
||||
const socket = new WebSocket('ws://localhost:8080/ws');
|
||||
const socket = new WebSocket('ws://localhost:8080');
|
||||
|
||||
// handle incoming messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import "../../css/users/globalstats.css";
|
||||
import "../../css/globalstats.css";
|
||||
|
||||
import WatchTimeStats from "./globalstats/watchtimestats";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import "../../../css/users/globalstats.css";
|
||||
import "../../../css/globalstats.css";
|
||||
|
||||
function WatchTimeStats(props) {
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.library-banner
|
||||
{
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.library-banner-image
|
||||
{
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
}
|
||||
12
src/pages/css/library/libraries.css
Normal file
12
src/pages/css/library/libraries.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.libraries-container
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||
}
|
||||
|
||||
/* .library-banner-image
|
||||
{
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
} */
|
||||
@@ -1,9 +1,54 @@
|
||||
|
||||
.library-card
|
||||
{
|
||||
/* border: solid 1px rgb(87, 87, 87); */
|
||||
border-radius: 4px;
|
||||
width: 400px;
|
||||
|
||||
/* width: 800px; */
|
||||
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
|
||||
.library-card-banner
|
||||
{
|
||||
width: 300px;
|
||||
width: 400px;
|
||||
height: 170px;
|
||||
background-color: black;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
border-radius: 4px 4px 0 0;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.library-card-banner:hover
|
||||
{
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.library-card-details
|
||||
{
|
||||
background-color: rgb(100, 100, 100,0.2);
|
||||
border-radius:0 0 4px 4px ;
|
||||
}
|
||||
|
||||
.library-card-details div
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.library-card-details div .label
|
||||
{
|
||||
color: #00A4DC;
|
||||
}
|
||||
|
||||
.library-card-details div p
|
||||
{
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.library-card
|
||||
.library-stat-card
|
||||
{
|
||||
width: 500px;
|
||||
height: 180px;
|
||||
|
||||
@@ -3,14 +3,7 @@
|
||||
margin-bottom: 20px;;
|
||||
}
|
||||
|
||||
.form-row
|
||||
{
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4,minmax(0,1fr));
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
||||
.settings-form {
|
||||
|
||||
@@ -31,12 +24,14 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* .settings-form div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
} */
|
||||
.form-row
|
||||
{
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3,minmax(0,1fr));
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
font-weight: bold;
|
||||
@@ -47,7 +42,6 @@
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
grid-column: span 2/span 2;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import "./css/libraries.css";
|
||||
import "./css/users/users.css";
|
||||
import "./css/library/libraries.css";
|
||||
// import "./css/users/users.css";
|
||||
|
||||
import Loading from "./components/general/loading";
|
||||
import LibraryCard from "./components/library/library-card";
|
||||
@@ -28,7 +28,7 @@ function Libraries() {
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
const url = `/api/getLibraries`;
|
||||
const url = `/stats/getLibraryStats`;
|
||||
axios
|
||||
.get(url)
|
||||
.then((data) => {
|
||||
@@ -59,13 +59,13 @@ function Libraries() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Activity">
|
||||
<div className="libraries">
|
||||
<h1>Libraries</h1>
|
||||
<div>
|
||||
<div className="libraries-container">
|
||||
{data &&
|
||||
data.map((item) => (
|
||||
|
||||
<LibraryCard data={item} base_url={config.hostUrl}/>
|
||||
<LibraryCard key={item.Id} data={item} base_url={config.hostUrl}/>
|
||||
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import './css/libraries.css';
|
||||
import './css/library/libraries.css';
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user