Stats Page

Added stats page, fixed auto init
This commit is contained in:
Thegan Govender
2023-04-01 12:36:28 +02:00
parent 04e990f522
commit 0b0e08b295
39 changed files with 2003 additions and 127 deletions

View File

@@ -100,6 +100,36 @@ router.post("/getLibraryItems", async (req, res) => {
console.log(`ENDPOINT CALLED: /getLibraryItems: `);
});
router.get("/getHistory", async (req, res) => {
try{
const { rows } = await db.query(
`SELECT * FROM jf_playback_activity order by "ActivityDateInserted" desc`
);
const groupedResults = {};
rows.forEach(row => {
if (groupedResults[row.NowPlayingItemId+row.EpisodeId]) {
groupedResults[row.NowPlayingItemId+row.EpisodeId].results.push(row);
} else {
groupedResults[row.NowPlayingItemId+row.EpisodeId] = {
...row,
results: []
};
}
});
res.send(Object.values(groupedResults));
}catch(error)
{
console.log(error);
}
});
router.get("/runWatchdog", async (req, res) => {
let message='Watchdog Started';
if(!process.env.WatchdogRunning )

View File

@@ -71,7 +71,6 @@ async function initDB()
pool.query(sql, (err, res) => {
if (err) throw err;
console.log('Database and table created');
pool.end();
});
});
} else {

View File

@@ -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-26 14:21:37 UTC
-- Started on 2023-04-01 09:50:04 UTC
SET statement_timeout = 0;
SET lock_timeout = 0;
@@ -19,7 +19,7 @@ SET client_min_messages = warning;
SET row_security = off;
--
-- TOC entry 251 (class 1255 OID 49412)
-- TOC entry 252 (class 1255 OID 49412)
-- Name: fs_last_library_activity(text); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -57,7 +57,7 @@ $$;
ALTER FUNCTION public.fs_last_library_activity(libraryid text) OWNER TO postgres;
--
-- TOC entry 247 (class 1255 OID 49383)
-- TOC entry 248 (class 1255 OID 49383)
-- Name: fs_last_user_activity(text); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -93,7 +93,7 @@ $$;
ALTER FUNCTION public.fs_last_user_activity(userid text) OWNER TO postgres;
--
-- TOC entry 245 (class 1255 OID 49411)
-- TOC entry 246 (class 1255 OID 49411)
-- Name: fs_library_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -145,7 +145,7 @@ $$;
ALTER FUNCTION public.fs_most_active_user(days integer) OWNER TO postgres;
--
-- TOC entry 249 (class 1255 OID 49386)
-- TOC entry 250 (class 1255 OID 49386)
-- Name: fs_most_played_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -186,7 +186,7 @@ $$;
ALTER FUNCTION public.fs_most_played_items(days integer, itemtype text) OWNER TO postgres;
--
-- TOC entry 250 (class 1255 OID 49394)
-- TOC entry 251 (class 1255 OID 49394)
-- Name: fs_most_popular_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -256,7 +256,7 @@ $$;
ALTER FUNCTION public.fs_most_used_clients(days integer) OWNER TO postgres;
--
-- TOC entry 248 (class 1255 OID 49385)
-- TOC entry 249 (class 1255 OID 49385)
-- Name: fs_most_viewed_libraries(integer); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -300,7 +300,7 @@ $$;
ALTER FUNCTION public.fs_most_viewed_libraries(days integer) OWNER TO postgres;
--
-- TOC entry 246 (class 1255 OID 49364)
-- TOC entry 247 (class 1255 OID 49364)
-- Name: fs_user_stats(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
--
@@ -324,6 +324,32 @@ $$;
ALTER FUNCTION public.fs_user_stats(hours integer, userid text) OWNER TO postgres;
--
-- TOC entry 245 (class 1255 OID 49418)
-- Name: fs_watch_stats_over_time(integer); Type: FUNCTION; Schema: public; Owner: postgres
--
CREATE FUNCTION public.fs_watch_stats_over_time(days integer) RETURNS TABLE("Date" date, "Count" bigint, "Library" text)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
l."Name" as "Library"
FROM jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
JOIN jf_libraries l ON i."ParentId" = l."Id"
WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
GROUP BY l."Name", DATE_TRUNC('day', a."ActivityDateInserted")
ORDER BY "Date";
END;
$$;
ALTER FUNCTION public.fs_watch_stats_over_time(days integer) OWNER TO postgres;
SET default_tablespace = '';
SET default_table_access_method = heap;
@@ -662,7 +688,7 @@ CREATE VIEW public.js_library_stats_overview AS
ALTER TABLE public.js_library_stats_overview OWNER TO postgres;
--
-- TOC entry 3236 (class 2606 OID 16401)
-- TOC entry 3237 (class 2606 OID 16401)
-- Name: app_config app_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
@@ -671,7 +697,7 @@ ALTER TABLE ONLY public.app_config
--
-- TOC entry 3238 (class 2606 OID 16419)
-- TOC entry 3239 (class 2606 OID 16419)
-- Name: jf_libraries jf_libraries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
@@ -680,7 +706,7 @@ ALTER TABLE ONLY public.jf_libraries
--
-- TOC entry 3244 (class 2606 OID 24912)
-- TOC entry 3245 (class 2606 OID 24912)
-- Name: jf_library_episodes jf_library_episodes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
@@ -689,7 +715,7 @@ ALTER TABLE ONLY public.jf_library_episodes
--
-- TOC entry 3240 (class 2606 OID 24605)
-- TOC entry 3241 (class 2606 OID 24605)
-- Name: jf_library_items jf_library_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
@@ -698,7 +724,7 @@ ALTER TABLE ONLY public.jf_library_items
--
-- TOC entry 3242 (class 2606 OID 24737)
-- TOC entry 3243 (class 2606 OID 24737)
-- Name: jf_library_seasons jf_library_seasons_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
@@ -707,7 +733,7 @@ ALTER TABLE ONLY public.jf_library_seasons
--
-- TOC entry 3246 (class 2606 OID 41737)
-- TOC entry 3247 (class 2606 OID 41737)
-- Name: jf_users jf_users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
--
@@ -716,7 +742,7 @@ ALTER TABLE ONLY public.jf_users
--
-- TOC entry 3390 (class 2618 OID 25163)
-- TOC entry 3391 (class 2618 OID 25163)
-- Name: jf_library_count_view _RETURN; Type: RULE; Schema: public; Owner: postgres
--
@@ -736,7 +762,7 @@ CREATE OR REPLACE VIEW public.jf_library_count_view AS
--
-- TOC entry 3247 (class 2606 OID 24617)
-- TOC entry 3248 (class 2606 OID 24617)
-- Name: jf_library_items jf_library_items_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
@@ -745,15 +771,15 @@ ALTER TABLE ONLY public.jf_library_items
--
-- TOC entry 3398 (class 0 OID 0)
-- Dependencies: 3247
-- TOC entry 3399 (class 0 OID 0)
-- Dependencies: 3248
-- 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-26 14:21:38 UTC
-- Completed on 2023-04-01 09:50:05 UTC
--
-- PostgreSQL database dump complete

View File

@@ -20,10 +20,10 @@ app.use('/api', apiRouter); // mount the API router at /api
app.use('/sync', syncRouter); // mount the API router at /sync
app.use('/stats', statsRouter); // mount the API router at /stats
app.listen(PORT, () => {
app.listen(PORT, async () => {
console.log(`Server listening on http://${LISTEN_IP}:${PORT}`);
try{
db.initDB();
await db.initDB();
ActivityMonitor.ActivityMonitor(1000);
}catch(error)
{

View File

@@ -280,6 +280,47 @@ router.post("/getLibraryLastPlayed", async (req, res) => {
}
});
router.post("/getViewsOverTime", async (req, res) => {
try {
const { days } = req.body;
let _days = days;
if (days=== undefined) {
_days = 30;
}
const { rows } = await db.query(
`select * from fs_watch_stats_over_time('${_days}')`
);
const reorganizedData = {};
rows.forEach((item) => {
const id = item.Library;
const count = item.Count;
const date = new Date(item.Date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: '2-digit'
});
if (!reorganizedData[id]) {
reorganizedData[id] = {
id,
data: []
};
}
reorganizedData[id].data.push({ x: date, y: count });
});
const finalData = Object.values(reorganizedData);
res.send(finalData);
} catch (error) {
console.log(error);
res.send(error);
}
});
module.exports = router;

View File

@@ -8,24 +8,26 @@ async function ActivityMonitor(interval) {
console.log("Activity Interval: " + interval);
const { rows: config } = await db.query(
'SELECT * FROM app_config where "ID"=1'
);
if(config.length===0)
{
return;
}
const base_url = config[0].JF_HOST;
const apiKey = config[0].JF_API_KEY;
if (base_url === null || config[0].JF_API_KEY === null) {
return;
}
setInterval(async () => {
try {
const { rows: config } = await db.query(
'SELECT * FROM app_config where "ID"=1'
);
if(config.length===0)
{
return;
}
const base_url = config[0].JF_HOST;
const apiKey = config[0].JF_API_KEY;
if (base_url === null || config[0].JF_API_KEY === null) {
return;
}
const url = `${base_url}/Sessions`;
const response = await axios.get(url, {
headers: {
@@ -165,7 +167,7 @@ async function ActivityMonitor(interval) {
} catch (error) {
console.log(error);
// console.log(error);
return [];
}
}, interval);

1169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,10 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.11.10",
"@nivo/api": "^0.74.1",
"@nivo/bar": "^0.80.0",
"@nivo/core": "^0.80.0",
"@nivo/line": "^0.80.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -18,6 +22,7 @@
"passport-jwt": "^4.0.1",
"pg": "^8.9.0",
"pg-promise": "^11.3.0",
"randomcolor": "^0.6.2",
"react": "^18.2.0",
"react-blurhash": "^0.3.0",
"react-dom": "^18.2.0",

View File

@@ -23,6 +23,9 @@ body
h1{
color: white;
font-weight: lighter;
margin: 0;
margin-top: 0.5em;
margin-bottom: 0.5em;
}

View File

@@ -13,7 +13,7 @@ import Loading from './pages/components/general/loading';
import Setup from './pages/setup';
import Navbar from './pages/components/general/navbar';
// import Navbar from './pages/components/general/navbar';
import Home from './pages/home';
import Settings from './pages/settings';
import Users from './pages/users';
@@ -24,6 +24,8 @@ import ErrorPage from './pages/components/general/error';
import Testing from './pages/testing';
import Activity from './pages/activity';
import Statistics from './pages/statistics';
function App() {
@@ -71,7 +73,7 @@ if (!config || config.apiKey ==null) {
return (
<div className="App">
<Navbar />
{/* <Navbar /> */}
<div>
<main>
<Routes>
@@ -81,6 +83,8 @@ if (!config || config.apiKey ==null) {
<Route path="/users/:UserId" element={<UserInfo />} />
<Route path="/libraries" element={<Libraries />} />
<Route path="/libraries/:LibraryId" element={<LibraryInfo />} />
<Route path="/statistics" element={<Statistics />} />
<Route path="/activity" element={<Activity />} />
<Route path="/testing" element={<Testing />} />
</Routes>
</main>

View File

@@ -20,24 +20,24 @@ code {
}
html {
body {
overflow: auto; /* show scrollbar when needed */
}
html::-webkit-scrollbar {
body::-webkit-scrollbar {
width: 10px; /* set scrollbar width */
}
html::-webkit-scrollbar-track {
body::-webkit-scrollbar-track {
background-color: transparent; /* set track color */
}
html::-webkit-scrollbar-thumb {
body::-webkit-scrollbar-thumb {
background-color: #8888884d; /* set thumb color */
border-radius: 5px; /* round corners */
}
html::-webkit-scrollbar-thumb:hover {
body::-webkit-scrollbar-thumb:hover {
background-color: #88888883; /* set thumb color */
}

View File

@@ -4,11 +4,14 @@ import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import Navbar from './pages/components/general/navbar';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Navbar/>
<App />
</BrowserRouter>
</React.StrictMode>

View File

@@ -2,12 +2,14 @@
import HomeFillIcon from 'remixicon-react/HomeFillIcon';
// import FileListFillIcon from 'remixicon-react/FileListFillIcon';
// import BarChartFillIcon from 'remixicon-react/BarChartFillIcon';
import BarChartFillIcon from 'remixicon-react/BarChartFillIcon';
import HistoryFillIcon from 'remixicon-react/HistoryFillIcon';
import SettingsFillIcon from 'remixicon-react/SettingsFillIcon';
import GalleryFillIcon from 'remixicon-react/GalleryFillIcon';
import UserFillIcon from 'remixicon-react/UserFillIcon';
import ReactjsFillIcon from 'remixicon-react/ReactjsFillIcon';
// import ReactjsFillIcon from 'remixicon-react/ReactjsFillIcon';
export const navData = [
{
@@ -18,27 +20,42 @@ export const navData = [
},
{
id: 1,
icon: <UserFillIcon />,
text: "Users",
link: "users"
},
{
id: 2,
icon: <GalleryFillIcon />,
text: "Libraries",
link: "libraries"
},
{
id: 2,
icon: <UserFillIcon />,
text: "Users",
link: "users"
},
{
id: 4,
icon: <ReactjsFillIcon />,
text: "Component Testing Playground",
link: "testing"
icon: <HistoryFillIcon />,
text: "Activity",
link: "activity"
},
{
id: 5,
icon: <BarChartFillIcon />,
text: "Statistics",
link: "statistics"
},
{
id: 6,
icon: <SettingsFillIcon />,
text: "Settings",
link: "settings"
}
]
]
// {
// id: 5,
// icon: <ReactjsFillIcon />,
// text: "Component Testing Playground",
// link: "testing"
// }
// ,

View File

@@ -1,75 +1,98 @@
import React, { useState, useEffect } from "react";
import API from "../classes/jellyfin-api";
import axios from "axios";
import "./css/activity.css";
import Config from "../lib/config";
import Loading from "./components/loading";
import ActivityTable from "./components/activity/activity-table";
import Loading from "./components/general/loading";
function Activity() {
const [data, setData] = useState([]);
const [data, setData] = useState();
const [config, setConfig] = useState(null);
const [itemCount,setItemCount] = useState(10);
useEffect(() => {
let _api = new API();
const fetchData = () => {
_api.getActivityData(15).then((ActivityData) => {
if (data && data.length > 0) {
const newDataOnly = ActivityData.Items.filter((item) => {
return !data.some((existingItem) => existingItem.Id === item.Id);
});
setData([
...newDataOnly,
...data.slice(0, data.length - newDataOnly.length),
]);
} else {
setData(ActivityData.Items);
const fetchConfig = async () => {
try {
const newConfig = await Config();
setConfig(newConfig);
} catch (error) {
if (error.code === "ERR_NETWORK") {
console.log(error);
}
});
}
};
const intervalId = setInterval(fetchData, 1000);
const fetchLibraries = () => {
const url = `/api/getHistory`;
axios
.get(url)
.then((data) => {
console.log("data");
setData(data.data);
console.log(data);
})
.catch((error) => {
console.log(error);
});
};
if (!config) {
fetchConfig();
}
if (!data) {
fetchLibraries();
}
const intervalId = setInterval(fetchLibraries, 60000 * 60);
return () => clearInterval(intervalId);
}, [data]);
}, [data, config]);
const options = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
};
if (!data || data.length === 0) {
if (!data) {
return <Loading />;
}
if (data.length === 0) {
return (<div>
<div className="Heading">
<h1>Activity</h1>
</div>
<div className="Activity">
<h1>No Activity to display</h1>
</div>
</div>
);
}
return (
<div>
<div className="Heading">
<h1>Activity</h1>
<div className="pagination-range">
<div className="header">Items</div>
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value);}}>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div className="Activity">
<ul>
{data &&
data.map((item) => (
<li
key={item.Id}
className={
data.findIndex((items) => items.Id === item.Id) <= 15
? "new"
: "old"
}
>
<div className="ActivityDetail"> {item.Name}</div>
<div className="ActivityTime">
{new Date(item.Date)
.toLocaleString("en-GB", options)
.replace(",", "")}
</div>
</li>
))}
</ul>
<div/>
<ActivityTable data={data} itemCount={itemCount}/>
</div>
</div>
);

View File

@@ -12,7 +12,7 @@ import MPMusic from "./statCards/mp_music";
import "../css/statCard.css";
function WatchStatistics() {
function HomeStatisticCards() {
const [days, setDays] = useState(30);
const [input, setInput] = useState(30);
@@ -64,4 +64,4 @@ function WatchStatistics() {
);
}
export default WatchStatistics;
export default HomeStatisticCards;

View File

@@ -0,0 +1,139 @@
import React ,{useState} from 'react';
import { Link } from "react-router-dom";
// import { useParams } from 'react-router-dom';
import '../../css/activity/activity-table.css';
function ActivityTable(props) {
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
const [currentPage, setCurrentPage] = useState(1);
const [data, setData] = useState(props.data);
function handleSort(key) {
const direction =
sortConfig.key === key && sortConfig.direction === "ascending"
? "descending"
: "ascending";
setSortConfig({ key, direction });
}
function sortData(data, { key, direction }) {
if (!key) return data;
const sortedData = [...data];
sortedData.sort((a, b) => {
if (a[key] < b[key]) return direction === "ascending" ? -1 : 1;
if (a[key] > b[key]) return direction === "ascending" ? 1 : -1;
return 0;
});
return sortedData;
}
const options = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
};
const sortedData = sortData(data, sortConfig);
const indexOfLastUser = currentPage * props.itemCount;
const indexOfFirstUser = indexOfLastUser - props.itemCount;
const currentData = sortedData.slice(indexOfFirstUser, indexOfLastUser);
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(sortedData.length / props.itemCount); i++) {
pageNumbers.push(i);
}
const handleCollapse = (itemId) => {
setData(data.map(item => {
if ((item.NowPlayingItemId+item.EpisodeId) === itemId) {
return { ...item, isCollapsed: !item.isCollapsed };
} else {
return item;
}
}));
}
return (
<div>
<div className='activity-table'>
<div className='table-headers'>
<div onClick={() => handleSort("UserName")}>User</div>
<div onClick={() => handleSort("NowPlayingItemName")}>Title </div>
<div onClick={() => handleSort("ActivityDateInserted")}>Date</div>
<div onClick={() => handleSort("results")}>Total Plays</div>
</div>
{currentData.map((item) => (
<div className='table-rows' key={item.NowPlayingItemId+item.EpisodeId} onClick={() => handleCollapse(item.NowPlayingItemId+item.EpisodeId)}>
<div className='table-rows-content'>
<div><Link to={`/users/${item.UserId}`}>{item.UserName}</Link></div>
<div>{!item.SeriesName ? item.NowPlayingItemName : item.SeriesName+' - '+ item.NowPlayingItemName}</div>
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(item.ActivityDateInserted))}</div>
<div>{item.results.length+1}</div>
</div>
<div className={`sub-table ${item.isCollapsed ? 'collapsed' : ''}`}>
{item.results.map((sub_item,index) => (
<div className='table-rows-content bg-grey sub-row' key={sub_item.EpisodeId+index}>
<div><Link to={`/users/${sub_item.UserId}`}>{sub_item.UserName}</Link></div>
<div>{!sub_item.SeriesName ? sub_item.NowPlayingItemName : sub_item.SeriesName+' - '+ sub_item.NowPlayingItemName}</div>
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(sub_item.ActivityDateInserted))}</div>
<div></div>
</div>
))}
</div>
</div>
))}
</div>
{props.itemCount>0 ?
<div className="pagination">
<button className="page-btn" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>
First
</button>
<button className="page-btn" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>
Previous
</button>
<div className="page-number">{`Page ${currentPage} of ${pageNumbers.length}`}</div>
<button className="page-btn" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === pageNumbers.length}>
Next
</button>
<button className="page-btn" onClick={() => setCurrentPage(pageNumbers.length)} disabled={currentPage === pageNumbers.length}>
Last
</button>
</div>
:<></>
}
</div>
);
}
export default ActivityTable;

View File

@@ -2,7 +2,8 @@ import { useParams } from 'react-router-dom';
import LibraryDetails from './library/library-details';
import LibraryGlobalStats from './library/library-stats';
import LastLibraryPlayed from './library/lastplayed';
import LibraryLastPlayed from './library/lastplayed';
import RecentlyPlayed from './library/recently-added';
@@ -15,7 +16,8 @@ function LibraryInfo() {
<div>
<LibraryDetails LibraryId={LibraryId}/>
<LibraryGlobalStats LibraryId={LibraryId}/>
<LastLibraryPlayed LibraryId={LibraryId}/>
<RecentlyPlayed LibraryId={LibraryId}/>
<LibraryLastPlayed LibraryId={LibraryId}/>
</div>
);
}

View File

@@ -28,7 +28,7 @@ function formatTime(time) {
}
function LastPlayedItem(props) {
function LastWatchedCard(props) {
const [loaded, setLoaded] = useState(false);
return (
<div className="last-card">
@@ -42,6 +42,7 @@ function LastPlayedItem(props) {
props.data.Id +
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
}
alt=""
onLoad={() => setLoaded(true)}
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
/>
@@ -63,4 +64,4 @@ function LastPlayedItem(props) {
);
}
export default LastPlayedItem;
export default LastWatchedCard;

View File

@@ -0,0 +1,37 @@
import React, {useState} from "react";
import { Blurhash } from 'react-blurhash';
import "../../../css/lastplayed.css";
function RecentlyAddedCard(props) {
const [loaded, setLoaded] = useState(false);
return (
<div className="last-card">
<div className="last-card-banner">
{loaded ? null : <Blurhash hash={props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]} width={'100%'} height={'100%'}/>}
<img
src={
`${
props.base_url +
"/Items/" +
props.data.Id +
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
}
alt=""
onLoad={() => setLoaded(true)}
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
/>
</div>
<div className="last-item-details">
<div className="last-item-name"> {props.data.Name}</div>
</div>
</div>
);
}
export default RecentlyAddedCard;

View File

@@ -1,12 +1,12 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import LastPlayedItem from "./lastplayed/last-played-item";
import ItemCardInfo from "./LastWatched/last-watched-card";
import Config from "../../../lib/config";
import "../../css/users/user-details.css";
function LastLibraryPlayed(props) {
function LibraryLastPlayed(props) {
const [data, setData] = useState();
const [config, setConfig] = useState();
@@ -56,7 +56,7 @@ function LastLibraryPlayed(props) {
<h1>Last Watched</h1>
<div className="last-played-container">
{data.map((item) => (
<LastPlayedItem data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
<ItemCardInfo data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
))}
</div>
@@ -65,4 +65,4 @@ function LastLibraryPlayed(props) {
);
}
export default LastLibraryPlayed;
export default LibraryLastPlayed;

View File

@@ -0,0 +1,72 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import RecentlyAddedCard from "./RecentlyAdded/recently-added-card";
import Config from "../../../lib/config";
import "../../css/users/user-details.css";
function RecentlyPlayed(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 {
let url=`${config.hostUrl}/users/5f63950a2339462196eb8cead70cae7e/Items/latest?parentId=${props.LibraryId}`;
const itemData = await axios.get(url, {
headers: {
"X-MediaBrowser-Token": config.apiKey,
},
});
console.log(itemData);
setData(itemData.data);
} catch (error) {
console.log(error);
}
};
if (!data) {
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>Recently Added</h1>
<div className="last-played-container">
{data.filter((item) => ["Series", "Movie","Audio"].includes(item.Type)).map((item) => (
<RecentlyAddedCard data={item} base_url={config.hostUrl} key={item.Id}/>
))}
</div>
</div>
);
}
export default RecentlyPlayed;

View File

@@ -108,7 +108,7 @@ function sessionCard(props) {
(props.data.session.NowPlayingItem.SeriesId
? props.data.session.NowPlayingItem.SeriesId
: props.data.session.NowPlayingItem.Id) +
"/Images/Backdrop?fillHeight=320&fillWidth=213&quality=50"
"/Images/Backdrop?fillHeight=320&fillWidth=213&quality=80"
})`,
}}
>
@@ -121,7 +121,7 @@ function sessionCard(props) {
(props.data.session.NowPlayingItem.SeriesId
? props.data.session.NowPlayingItem.SeriesId
: props.data.session.NowPlayingItem.Id) +
"/Images/Primary?quality=50"
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"
}
alt=""
></img>

View File

@@ -15,6 +15,7 @@ function ItemImage(props) {
(props.data.Id) +
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"
}
alt=""
onLoad={() => setLoaded(true)}
/>
</div>

View File

@@ -12,7 +12,7 @@ function StatComponent(props) {
<div className="stats-list">
{props.data &&
props.data.map((item, index) => (
<div className="stat-item" key={item.Id}>
<div className="stat-item" key={item.Id || index}>
<p className="stat-item-index">{index + 1}</p>
{item.UserId ?
<p className="stat-item-name"> <Link to={`/users/${item.UserId}`}> {item.Name}</Link> </p>

View File

@@ -0,0 +1,154 @@
import React,{useState,useEffect} from 'react';
import axios from 'axios';
import { ResponsiveLine } from '@nivo/line';
import '../../css/stats.css';
function DailyPlayStats(props) {
const [data, setData] = useState();
const [days, setDays] = useState(60);
useEffect(() => {
const fetchLibraries = () => {
const url = `/stats/getViewsOverTime`;
axios
.post(url, {days:days}, {
headers: {
"Content-Type": "application/json",
},
})
.then((data) => {
setData(data.data);
})
.catch((error) => {
console.log(error);
});
};
if (!data) {
fetchLibraries();
}
if (days !== props.days) {
setDays(props.days);
fetchLibraries();
}
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [data, days,props.days]);
if (!data) {
return <></>;
}
if (data.length === 0) {
return (<div className="statistics-widget">
<h1>Daily Play Count Per Library - {days} Days</h1>
<h1>No Stats to display</h1>
</div>
);
}
return (
<div className="statistics-widget" >
<h1>Daily Play Count Per Library - {days} Days</h1>
<ResponsiveLine
data={data}
margin={{ top: 50, right: 100, bottom: 150, left: 50 }}
xScale={{ type: 'point' }}
yScale={{
type: 'linear',
min: 'auto',
max: 'auto',
stacked: false,
reverse: false
}}
enableGridX={false}
enableSlices={"x"}
yFormat=" >-.0f"
curve="natural"
axisBottom={{
orient: 'bottom',
tickSize: 5,
tickPadding: 10,
tickRotation: 0,
legend: 'Days',
legendOffset: 36,
legendPosition: 'middle',
itemTextColor: '#fff'
}}
axisLeft={{
orient: 'left',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'Views',
legendOffset: -40,
legendPosition: 'middle',
itemTextColor: '#fff',
}}
colors={{ scheme: 'category10' }}
pointSize={10}
pointColor={{ theme: 'background' }}
pointBorderWidth={2}
pointBorderColor={{ from: 'serieColor' }}
pointLabelYOffset={-12}
useMesh={true}
legends={[
{
itemTextColor: '#fff',
anchor: 'bottom',
direction: 'row',
justify: false,
translateX: 0,
translateY: 80,
itemsSpacing: 0,
itemDirection: 'left-to-right',
itemWidth: 80,
itemHeight: 20,
itemOpacity: 0.75,
symbolSize: 12,
symbolShape: 'circle',
symbolBorderColor: 'rgba(0, 0, 0, .5)',
effects: [
{
on: 'hover',
style: {
itemBackground: 'rgba(0, 0, 0, .03)',
itemOpacity: 1
}
}
]
}
]}
/>
</div>
);
}
export default DailyPlayStats;

View File

@@ -43,6 +43,7 @@ function LastPlayedItem(props) {
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
}
onLoad={() => setLoaded(true)}
alt=""
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
/>
</div>

View File

@@ -0,0 +1,98 @@
div a
{
text-decoration: none;
color: white;
}
.table-rows:hover
{
background-color: rgba(0, 0, 0, 0.4);
}
.table-rows-content:hover a
{
color: #00A4DC;
}
.activity-table
{
background-color: rgba(0, 0, 0, 0.2);
}
.table-rows-content{
margin-bottom: 10px;
}
.table-headers div {
background-color: rgba(0, 0, 0, 0.8);
border-bottom: 1px solid transparent;
border-right: 1px solid rgba(255, 255, 255, 0.05);
font-size: 1.2em;
cursor: pointer;
}
.table-headers div:hover {
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
}
.table-headers, .table-rows-content
{
display: flex;
justify-content: space-between;
/* border-bottom: 1px solid rgba(255, 255, 255, 0.05); */
}
.table-headers div,
.table-rows-content div
{
border-right: 1px solid rgba(255, 255, 255, 0.05);
width: 100%;
padding: 10px;
}
.table-headers div:last-child,
.table-rows-content div:last-child
{
border-right: none;
}
.sub-table {
overflow: hidden;
max-height: 0; /* set the height to 0 to collapse the div */
opacity:0;
transition: all 0.3s ease;
}
.collapsed {
transition: all 0.3s ease;
opacity: 100;
max-height: 500px;
}
.sub-row{
color: darkgray;
margin-bottom: 0;
}
.sub-row a{
color: darkgray;
}
.sub-row a:hover{
color: #00A4DC;
}
.sub-row:last-child
{
margin-bottom: 50px;
}
.bg-grey
{
background-color: rgb(100, 100, 100,0.2);
}

View File

@@ -8,7 +8,7 @@
margin-right: 20px;
color: white;
margin-bottom: 20px;
min-height: 350px;
min-height: 300px;

View File

@@ -2,6 +2,7 @@
{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
}
/* .library-banner-image

View File

@@ -8,6 +8,7 @@
/* width: 800px; */
margin-bottom: 40px;
}

View File

@@ -5,6 +5,8 @@
background-color: #5a2da5;
/* background: linear-gradient(to right, #AA5CC3,#00A4DC); */
height: 50px;
/* position: sticky;
top: 0; */
}

View File

@@ -7,7 +7,7 @@
.sessions-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(520px, 520px));
grid-auto-rows: 235px;/* max-width+offset so 215 + 20*/
grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/
margin-right: 20px;
}
@@ -19,7 +19,7 @@
background-color: grey;
/* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */
max-height: 215px;
max-height: 180px;
max-width: 500px;
/* margin-left: 20px; */

View File

@@ -12,7 +12,7 @@
.Heading h1
{
padding-right: 10px;
margin: 0;
/* margin: 0; */
}
.stat-cards-container
@@ -141,6 +141,7 @@
font-size: 1.2em;
align-self: flex-end;
justify-content: space-evenly;
margin-bottom: 0.9em;
}

12
src/pages/css/stats.css Normal file
View File

@@ -0,0 +1,12 @@
.statistics-widget
{
height: 700px;
color:black !important;
background-color:rgba(0,0,0,0.4);
padding:20px;
margin:20px;
border-radius:4px;
/* text-align: center; */
}

View File

@@ -24,9 +24,11 @@
{
padding: 15px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
td a{
text-decoration: none;
color: white;
@@ -37,8 +39,11 @@ td:hover a{
color: rgb(0, 164, 219);
}
th {
background-color: rgba(0, 0, 0, 0.8);
border-right: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
}
@@ -121,6 +126,7 @@ td:first-child {
font-size: 1.2em;
align-self: flex-end;
justify-content: space-between;
margin-bottom: 0.9em;
}
.pagination-range select

View File

@@ -3,7 +3,7 @@ import React from 'react'
import './css/home.css'
import Sessions from './components/sessions/sessions'
import WatchStatistics from './components/WatchStatistics'
import HomeStatisticCards from './components/HomeStatisticCards'
import LibraryOverView from './components/libraryOverview'
@@ -12,7 +12,7 @@ export default function Home() {
<div>
<Sessions />
<WatchStatistics/>
<HomeStatisticCards/>
<LibraryOverView/>
</div>

View File

@@ -12,7 +12,6 @@ import LibraryCard from "./components/library/library-card";
function Libraries() {
const [data, setData] = useState();
// const [items, setItems] = useState([]);
const [config, setConfig] = useState(null);
useEffect(() => {

8
src/pages/line-chart.js Normal file
View File

@@ -0,0 +1,8 @@
import { LineChart, Line } from 'recharts';
const data = [{name: 'Page A', uv: 400, pv: 2400, amt: 2400}, ...];
const renderLineChart = (
<LineChart width={400} height={400} data={data}>
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
</LineChart>
);

19
src/pages/statistics.js Normal file
View File

@@ -0,0 +1,19 @@
import React,{useState} from 'react';
// import './css/library/libraries.css';
import DailyPlayStats from './components/statistics/daily-play-count';
function Statistics(props) {
const [days, setDays] = useState(60);
return (
<DailyPlayStats days={days}/>
);
}
export default Statistics;