mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
UI Changes, stat fixes
Changed around nav bar Added users stats view renamed views for better understanding(still more to do). CREATED SCRIPT TO SPINUP POSTGRESS DB WITH TABLES AND FUNCTIONS
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
-- Database: jfstat
|
||||
|
||||
-- DROP DATABASE IF EXISTS jfstat;
|
||||
|
||||
CREATE DATABASE jfstat
|
||||
WITH
|
||||
OWNER = jfstat
|
||||
ENCODING = 'UTF8'
|
||||
LC_COLLATE = 'en_US.utf8'
|
||||
LC_CTYPE = 'en_US.utf8'
|
||||
TABLESPACE = pg_default
|
||||
CONNECTION LIMIT = -1
|
||||
IS_TEMPLATE = False;
|
||||
@@ -1,27 +0,0 @@
|
||||
-- Table: public.jf_activity_watchdog
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_activity_watchdog;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_activity_watchdog
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"IsPaused" boolean DEFAULT false,
|
||||
"UserId" text COLLATE pg_catalog."default",
|
||||
"UserName" text COLLATE pg_catalog."default",
|
||||
"Client" text COLLATE pg_catalog."default",
|
||||
"DeviceName" text COLLATE pg_catalog."default",
|
||||
"DeviceId" text COLLATE pg_catalog."default",
|
||||
"ApplicationVersion" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemId" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemName" text COLLATE pg_catalog."default",
|
||||
"SeasonId" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
"EpisodeId" text COLLATE pg_catalog."default",
|
||||
"PlaybackDuration" bigint,
|
||||
"ActivityDateInserted" timestamp with time zone
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_activity_watchdog
|
||||
OWNER to postgres;
|
||||
@@ -1,32 +0,0 @@
|
||||
-- Table: public.jf_library_episodes
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_library_episodes;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_library_episodes
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"EpisodeId" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default",
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"PremiereDate" timestamp with time zone,
|
||||
"OfficialRating" text COLLATE pg_catalog."default",
|
||||
"CommunityRating" double precision,
|
||||
"RunTimeTicks" bigint,
|
||||
"ProductionYear" integer,
|
||||
"IndexNumber" integer,
|
||||
"ParentIndexNumber" integer,
|
||||
"Type" text COLLATE pg_catalog."default",
|
||||
"ParentLogoItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropImageTags" text COLLATE pg_catalog."default",
|
||||
"SeriesId" text COLLATE pg_catalog."default",
|
||||
"SeasonId" text COLLATE pg_catalog."default",
|
||||
"SeasonName" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT jf_library_episodes_pkey PRIMARY KEY ("Id")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_library_episodes
|
||||
OWNER to postgres;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- Table: public.app_config
|
||||
|
||||
-- DROP TABLE IF EXISTS public.app_config;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.app_config
|
||||
(
|
||||
"ID" integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||
"JF_HOST" text COLLATE pg_catalog."default",
|
||||
"JF_API_KEY" text COLLATE pg_catalog."default",
|
||||
"APP_USER" text COLLATE pg_catalog."default",
|
||||
"APP_PASSWORD" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT app_config_pkey PRIMARY KEY ("ID")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.app_config
|
||||
OWNER to postgres;
|
||||
@@ -1,20 +0,0 @@
|
||||
-- Table: public.jf_libraries
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_libraries;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_libraries
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"IsFolder" boolean NOT NULL DEFAULT true,
|
||||
"Type" text COLLATE pg_catalog."default" NOT NULL DEFAULT 'CollectionFolder'::text,
|
||||
"CollectionType" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"ImageTagsPrimary" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT jf_libraries_pkey PRIMARY KEY ("Id")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_libraries
|
||||
OWNER to postgres;
|
||||
@@ -1,38 +0,0 @@
|
||||
-- Table: public.jf_library_items
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_library_items;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_library_items
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"PremiereDate" timestamp with time zone,
|
||||
"EndDate" timestamp with time zone,
|
||||
"CommunityRating" double precision,
|
||||
"RunTimeTicks" bigint,
|
||||
"ProductionYear" integer,
|
||||
"IsFolder" boolean,
|
||||
"Type" text COLLATE pg_catalog."default",
|
||||
"Status" text COLLATE pg_catalog."default",
|
||||
"ImageTagsPrimary" text COLLATE pg_catalog."default",
|
||||
"ImageTagsBanner" text COLLATE pg_catalog."default",
|
||||
"ImageTagsLogo" text COLLATE pg_catalog."default",
|
||||
"ImageTagsThumb" text COLLATE pg_catalog."default",
|
||||
"BackdropImageTags" text COLLATE pg_catalog."default",
|
||||
"ParentId" text COLLATE pg_catalog."default" NOT NULL,
|
||||
CONSTRAINT jf_library_items_pkey PRIMARY KEY ("Id"),
|
||||
CONSTRAINT jf_library_items_fkey FOREIGN KEY ("ParentId")
|
||||
REFERENCES public.jf_libraries ("Id") MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL
|
||||
NOT VALID
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_library_items
|
||||
OWNER to postgres;
|
||||
|
||||
COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items
|
||||
IS 'jf_library';
|
||||
@@ -1,24 +0,0 @@
|
||||
-- Table: public.jf_library_seasons
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_library_seasons;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_library_seasons
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default",
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"IndexNumber" integer,
|
||||
"Type" text COLLATE pg_catalog."default",
|
||||
"ParentLogoItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropImageTags" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
"SeriesId" text COLLATE pg_catalog."default",
|
||||
"SeriesPrimaryImageTag" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT jf_library_seasons_pkey PRIMARY KEY ("Id")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_library_seasons
|
||||
OWNER to postgres;
|
||||
@@ -1,27 +0,0 @@
|
||||
-- Table: public.jf_playback_activity
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_playback_activity;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_playback_activity
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"IsPaused" boolean DEFAULT false,
|
||||
"UserId" text COLLATE pg_catalog."default",
|
||||
"UserName" text COLLATE pg_catalog."default",
|
||||
"Client" text COLLATE pg_catalog."default",
|
||||
"DeviceName" text COLLATE pg_catalog."default",
|
||||
"DeviceId" text COLLATE pg_catalog."default",
|
||||
"ApplicationVersion" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemId" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemName" text COLLATE pg_catalog."default",
|
||||
"SeasonId" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
"EpisodeId" text COLLATE pg_catalog."default",
|
||||
"PlaybackDuration" bigint,
|
||||
"ActivityDateInserted" timestamp with time zone
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_playback_activity
|
||||
OWNER to postgres;
|
||||
@@ -1,45 +0,0 @@
|
||||
-- FUNCTION: public.fs_most_played_items(integer, text)
|
||||
|
||||
-- DROP FUNCTION IF EXISTS public.fs_most_played_items(integer, text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_most_played_items(
|
||||
days integer,
|
||||
itemtype text)
|
||||
RETURNS TABLE("Plays" bigint, total_playback_duration numeric, "Name" text, "Id" text)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.plays,
|
||||
t.total_playback_duration,
|
||||
i."Name",
|
||||
i."Id"
|
||||
FROM (
|
||||
SELECT
|
||||
count(*) AS plays,
|
||||
sum(jf_playback_activity."PlaybackDuration") AS total_playback_duration,
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
FROM
|
||||
jf_playback_activity
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
GROUP BY
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY
|
||||
count(*) DESC
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON t."NowPlayingItemId" = i."Id"
|
||||
AND i."Type" = itemType
|
||||
ORDER BY
|
||||
t.plays DESC;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
ALTER FUNCTION public.fs_most_played_items(integer, text)
|
||||
OWNER TO postgres;
|
||||
@@ -1,52 +0,0 @@
|
||||
-- FUNCTION: public.fs_most_popular_items(integer, text)
|
||||
|
||||
-- DROP FUNCTION IF EXISTS public.fs_most_popular_items(integer, text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_most_popular_items(
|
||||
days integer,
|
||||
itemtype text)
|
||||
RETURNS TABLE(unique_viewers bigint, latest_activity_date timestamp with time zone, "Name" text, "Id" text)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.unique_viewers,
|
||||
t.latest_activity_date,
|
||||
i."Name",
|
||||
i."Id"
|
||||
FROM (
|
||||
SELECT
|
||||
jf_playback_activity."NowPlayingItemId",
|
||||
count(DISTINCT jf_playback_activity."UserId") AS unique_viewers,
|
||||
latest_activity_date.latest_date AS latest_activity_date
|
||||
FROM
|
||||
jf_playback_activity
|
||||
JOIN (
|
||||
SELECT
|
||||
jf_playback_activity_1."NowPlayingItemId",
|
||||
max(jf_playback_activity_1."ActivityDateInserted") AS latest_date
|
||||
FROM
|
||||
jf_playback_activity jf_playback_activity_1
|
||||
GROUP BY jf_playback_activity_1."NowPlayingItemId"
|
||||
) latest_activity_date
|
||||
ON jf_playback_activity."NowPlayingItemId" = latest_activity_date."NowPlayingItemId"
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
GROUP BY
|
||||
jf_playback_activity."NowPlayingItemId", latest_activity_date.latest_date
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON t."NowPlayingItemId" = i."Id"
|
||||
AND i."Type" = itemType
|
||||
ORDER BY
|
||||
t.unique_viewers DESC, t.latest_activity_date DESC;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
ALTER FUNCTION public.fs_most_popular_items(integer, text)
|
||||
OWNER TO postgres;
|
||||
@@ -1,48 +0,0 @@
|
||||
-- FUNCTION: public.fs_most_viewed_libraries(integer)
|
||||
|
||||
-- DROP FUNCTION IF EXISTS public.fs_most_viewed_libraries(integer);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_most_viewed_libraries(
|
||||
days integer)
|
||||
RETURNS TABLE("Plays" numeric, "Id" text, "Name" text, "ServerId" text, "IsFolder" boolean, "Type" text, "CollectionType" text, "ImageTagsPrimary" text)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
sum(t."Plays"),
|
||||
l."Id",
|
||||
l."Name",
|
||||
l."ServerId",
|
||||
l."IsFolder",
|
||||
l."Type",
|
||||
l."CollectionType",
|
||||
l."ImageTagsPrimary"
|
||||
FROM (
|
||||
SELECT count(*) AS "Plays",
|
||||
sum(jf_playback_activity."PlaybackDuration") AS "TotalPlaybackDuration",
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
FROM jf_playback_activity
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
|
||||
GROUP BY jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY "Plays" DESC
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON i."Id" = t."NowPlayingItemId"
|
||||
JOIN jf_libraries l
|
||||
ON l."Id" = i."ParentId"
|
||||
GROUP BY
|
||||
l."Id"
|
||||
ORDER BY
|
||||
(sum( t."Plays")) DESC;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
ALTER FUNCTION public.fs_most_viewed_libraries(integer)
|
||||
OWNER TO postgres;
|
||||
@@ -1,11 +0,0 @@
|
||||
-- Role: jfstat
|
||||
-- DROP ROLE IF EXISTS jfstat;
|
||||
|
||||
CREATE ROLE jfstat WITH
|
||||
LOGIN
|
||||
SUPERUSER
|
||||
INHERIT
|
||||
CREATEDB
|
||||
CREATEROLE
|
||||
NOREPLICATION
|
||||
ENCRYPTED PASSWORD 'SCRAM-SHA-256$4096:Cf1EY3ozsXG1sR/TWv/Xcw==$Om2f07jurCEEyaOGV/Fju9AGtUVj67Q1JFm0AZSiK4M=:lFaFNHdvtEHzC8l5qUf/uAWENJHa1T9jM3Bv5WDz66E=';
|
||||
@@ -1,22 +0,0 @@
|
||||
-- View: public.jf_library_count_view
|
||||
|
||||
-- DROP VIEW public.jf_library_count_view;
|
||||
|
||||
CREATE OR REPLACE VIEW public.jf_library_count_view
|
||||
AS
|
||||
SELECT l."Id",
|
||||
l."Name",
|
||||
l."CollectionType",
|
||||
count(DISTINCT i."Id") AS "Library_Count",
|
||||
count(DISTINCT s."Id") AS "Season_Count",
|
||||
count(DISTINCT e."Id") AS "Episode_Count"
|
||||
FROM jf_libraries l
|
||||
JOIN jf_library_items i ON i."ParentId" = l."Id"
|
||||
LEFT JOIN jf_library_seasons s ON s."SeriesId" = i."Id"
|
||||
LEFT JOIN jf_library_episodes e ON e."SeasonId" = s."Id"
|
||||
GROUP BY l."Id", l."Name"
|
||||
ORDER BY (count(DISTINCT i."Id")) DESC;
|
||||
|
||||
ALTER TABLE public.jf_library_count_view
|
||||
OWNER TO postgres;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
-- View: public.js_most_active_user
|
||||
|
||||
-- DROP VIEW public.js_most_active_user;
|
||||
|
||||
CREATE OR REPLACE VIEW public.js_most_active_user
|
||||
AS
|
||||
SELECT count(*) AS "Plays",
|
||||
jf_playback_activity."UserId",
|
||||
jf_playback_activity."UserName"
|
||||
FROM jf_playback_activity
|
||||
GROUP BY jf_playback_activity."UserId", jf_playback_activity."UserName"
|
||||
ORDER BY (count(*)) DESC;
|
||||
|
||||
ALTER TABLE public.js_most_active_user
|
||||
OWNER TO postgres;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
-- View: public.js_most_used_clients
|
||||
|
||||
-- DROP VIEW public.js_most_used_clients;
|
||||
|
||||
CREATE OR REPLACE VIEW public.js_most_used_clients
|
||||
AS
|
||||
SELECT count(*) AS "Plays",
|
||||
jf_playback_activity."Client"
|
||||
FROM jf_playback_activity
|
||||
GROUP BY jf_playback_activity."Client"
|
||||
ORDER BY (count(*)) DESC;
|
||||
|
||||
ALTER TABLE public.js_most_used_clients
|
||||
OWNER TO postgres;
|
||||
|
||||
@@ -20,8 +20,17 @@ router.get("/getconfig", async (req, res) => {
|
||||
router.post("/setconfig", async (req, res) => {
|
||||
const { JF_HOST, JF_API_KEY } = req.body;
|
||||
|
||||
const { rows:getConfig } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
|
||||
let query='UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1';
|
||||
if(getConfig.length===0)
|
||||
{
|
||||
query='INSERT INTO app_config ("JF_HOST","JF_API_KEY","APP_USER","APP_PASSWORD") VALUES ($1,$2,null,null)';
|
||||
}
|
||||
|
||||
|
||||
const { rows } = await db.query(
|
||||
'UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1',
|
||||
query,
|
||||
[JF_HOST, JF_API_KEY]
|
||||
);
|
||||
console.log({ JF_HOST: JF_HOST, JF_API_KEY: JF_API_KEY });
|
||||
|
||||
@@ -13,6 +13,7 @@ const jf_activity_watchdog_columns = [
|
||||
"SeasonId",
|
||||
"SeriesName",
|
||||
"PlaybackDuration",
|
||||
"PlayMethod",
|
||||
"ActivityDateInserted",
|
||||
];
|
||||
|
||||
@@ -32,6 +33,7 @@ const jf_activity_watchdog_columns = [
|
||||
SeasonId: item.NowPlayingItem.SeasonId || null,
|
||||
SeriesName: item.NowPlayingItem.SeriesName || null,
|
||||
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0,
|
||||
PlayMethod:item.PlayState.PlayMethod,
|
||||
ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"SeasonId",
|
||||
"SeriesName",
|
||||
"PlaybackDuration",
|
||||
"PlayMethod",
|
||||
"ActivityDateInserted",
|
||||
];
|
||||
|
||||
@@ -33,6 +34,7 @@
|
||||
SeasonId: item.NowPlayingItem.SeasonId || null,
|
||||
SeriesName: item.NowPlayingItem.SeriesName || null,
|
||||
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0,
|
||||
PlayMethod: item.PlayState.PlayMethod !== undefined ? item.PlayState.PlayMethod : item.PlayMethod ,
|
||||
ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
||||
23
backend/models/jf_users.js
Normal file
23
backend/models/jf_users.js
Normal file
@@ -0,0 +1,23 @@
|
||||
////////////////////////// pn delete move to playback
|
||||
const jf_users_columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"PrimaryImageTag",
|
||||
"LastLoginDate",
|
||||
"LastActivityDate",
|
||||
"IsAdministrator"
|
||||
];
|
||||
|
||||
const jf_users_mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
PrimaryImageTag: item.PrimaryImageTag,
|
||||
LastLoginDate: item.LastLoginDate,
|
||||
LastActivityDate: item.LastActivityDate,
|
||||
IsAdministrator: item.Policy.IsAdministrator,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_users_columns,
|
||||
jf_users_mapping,
|
||||
};
|
||||
@@ -45,6 +45,20 @@ router.post("/getMostViewedMovies", async (req, res) => {
|
||||
|
||||
});
|
||||
|
||||
router.post("/getMostViewedMusic", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_played_items(${_days},'Audio') limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.post("/getMostViewedLibraries", async (req, res) => {
|
||||
@@ -61,17 +75,38 @@ router.post("/getMostViewedLibraries", async (req, res) => {
|
||||
|
||||
});
|
||||
|
||||
router.get("/getMostUsedClient", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM js_most_used_clients limit 5');
|
||||
|
||||
|
||||
router.post("/getMostUsedClient", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_used_clients(${_days}) limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
});
|
||||
|
||||
router.get("/getMostActiveUsers", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM js_most_active_user limit 5');
|
||||
|
||||
|
||||
router.post("/getMostActiveUsers", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_active_user(${_days}) limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.post("/getMostPopularMovies", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
@@ -95,7 +130,7 @@ router.post("/getMostPopularSeries", async (req, res) => {
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
console.log(`select * from fs_most_popular_items(${_days},'Series') limit 5`);
|
||||
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_popular_items(${_days},'Series') limit 5`
|
||||
);
|
||||
@@ -103,6 +138,21 @@ router.post("/getMostPopularSeries", async (req, res) => {
|
||||
|
||||
});
|
||||
|
||||
router.post("/getMostPopularMusic", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_popular_items(${_days},'Audio') limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
|
||||
router.get("/getPlaybackActivity", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM jf_playback_activity');
|
||||
@@ -110,4 +160,10 @@ router.get("/getPlaybackActivity", async (req, res) => {
|
||||
// console.log(`ENDPOINT CALLED: /getPlaybackActivity`);
|
||||
});
|
||||
|
||||
router.get("/getAllUserActivity", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM jf_all_user_activity');
|
||||
res.send(rows);
|
||||
// console.log(`ENDPOINT CALLED: /getPlaybackActivity`);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
585
backend/sync.js
585
backend/sync.js
@@ -8,22 +8,12 @@ const sendMessageToClients = ws(8080);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const {
|
||||
jf_libraries_columns,
|
||||
jf_libraries_mapping,
|
||||
} = require("./models/jf_libraries");
|
||||
const {
|
||||
jf_library_items_columns,
|
||||
jf_library_items_mapping,
|
||||
} = require("./models/jf_library_items");
|
||||
const {
|
||||
jf_library_seasons_columns,
|
||||
jf_library_seasons_mapping,
|
||||
} = require("./models/jf_library_seasons");
|
||||
const {
|
||||
jf_library_episodes_columns,
|
||||
jf_library_episodes_mapping,
|
||||
} = require("./models/jf_library_episodes");
|
||||
const {jf_libraries_columns,jf_libraries_mapping,} = require("./models/jf_libraries");
|
||||
const {jf_library_items_columns,jf_library_items_mapping,} = require("./models/jf_library_items");
|
||||
const {jf_library_seasons_columns,jf_library_seasons_mapping,} = require("./models/jf_library_seasons");
|
||||
const {jf_library_episodes_columns,jf_library_episodes_mapping,} = require("./models/jf_library_episodes");
|
||||
|
||||
const {jf_users_columns,jf_users_mapping,} = require("./models/jf_users");
|
||||
|
||||
/////////////////////////////////////////Functions
|
||||
class sync {
|
||||
@@ -32,6 +22,22 @@ class sync {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
async getUsers() {
|
||||
try {
|
||||
const url = `${this.hostUrl}/Users`;
|
||||
console.log("getAdminUser: ", url);
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": this.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getAdminUser() {
|
||||
try {
|
||||
const url = `${this.hostUrl}/Users`;
|
||||
@@ -51,10 +57,9 @@ class sync {
|
||||
}
|
||||
}
|
||||
|
||||
async getItem(itemID) {
|
||||
async getItem(itemID,userid) {
|
||||
try {
|
||||
const admins = await this.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
|
||||
let url = `${this.hostUrl}/users/${userid}/Items`;
|
||||
if (itemID !== undefined) {
|
||||
url += `?ParentID=${itemID}`;
|
||||
@@ -68,7 +73,7 @@ class sync {
|
||||
const results = response.data.Items;
|
||||
if (itemID === undefined) {
|
||||
return results.filter((type) =>
|
||||
["tvshows", "movies"].includes(type.CollectionType)
|
||||
["tvshows", "movies","music"].includes(type.CollectionType)
|
||||
);
|
||||
} else {
|
||||
return results;
|
||||
@@ -78,11 +83,10 @@ class sync {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async getSeasonsAndEpisodes(showId) {
|
||||
async getSeasonsAndEpisodes(showId,userid) {
|
||||
const allSeasons = [];
|
||||
const allEpisodes = [];
|
||||
|
||||
let seasonItems = await this.getItem(showId);
|
||||
let seasonItems = await this.getItem(showId,userid);
|
||||
const seasonWithParent = seasonItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: showId },
|
||||
@@ -90,7 +94,7 @@ class sync {
|
||||
allSeasons.push(...seasonWithParent);
|
||||
for (let e = 0; e < seasonItems.length; e++) {
|
||||
const season = seasonItems[e];
|
||||
let episodeItems = await this.getItem(season.Id);
|
||||
let episodeItems = await this.getItem(season.Id,userid);
|
||||
const episodeWithParent = episodeItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: season.Id },
|
||||
@@ -103,9 +107,8 @@ class sync {
|
||||
}
|
||||
////////////////////////////////////////API Methods
|
||||
|
||||
///////////////////////////////////////writeLibraries
|
||||
router.get("/writeLibraries", async (req, res) => {
|
||||
let message = [];
|
||||
///////////////////////////////////////Write Users
|
||||
router.get("/writeUsers", async (req, res) => {
|
||||
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
@@ -115,120 +118,113 @@ router.get("/writeLibraries", async (req, res) => {
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const data = await _sync.getItem(); //getting all root folders aka libraries
|
||||
|
||||
// specify the columns to insert into
|
||||
const data = await _sync.getUsers();
|
||||
|
||||
const existingIds = await db
|
||||
.query('SELECT "Id" FROM jf_libraries')
|
||||
.query('SELECT "Id" FROM jf_users')
|
||||
.then((res) => res.rows.map((row) => row.Id)); // get existing library Ids from the db
|
||||
|
||||
//data mapping
|
||||
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
dataToInsert = await data.map(jf_libraries_mapping);
|
||||
dataToInsert = await data.map(jf_users_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
dataToInsert = await data
|
||||
.filter((row) => !existingIds.includes(row.Id))
|
||||
.map(jf_libraries_mapping);
|
||||
.map(jf_users_mapping);
|
||||
}
|
||||
|
||||
//Bulkinsert new data not on db
|
||||
if (dataToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
dataToInsert,
|
||||
jf_libraries_columns,
|
||||
"jf_libraries"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: dataToInsert.length + " Rows Inserted.",
|
||||
});
|
||||
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
sendMessageToClients({
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk insert" });
|
||||
sendMessageToClients({ Message: "No new data to bulk insert" });
|
||||
let result = await db.insertBulk("jf_users",dataToInsert,jf_users_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIds.length > data.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const AllIds = data.map((row) => row.Id);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_libraries WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: existingIds.length - data.length + " Rows Removed.",
|
||||
});
|
||||
sendMessageToClients(
|
||||
existingIds.length - data.length + " Rows Removed."
|
||||
);
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
sendMessageToClients({
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk delete" });
|
||||
sendMessageToClients({ Message: "No new data to bulk delete" });
|
||||
|
||||
const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
if (toDeleteIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_users",toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(toDeleteIds.length + " Rows Removed.");
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
//Sent logs
|
||||
|
||||
res.send(message);
|
||||
res.send();
|
||||
});
|
||||
|
||||
///////////////////////////////////////writeLibraries
|
||||
router.get("/writeLibraries", async (req, res) => {
|
||||
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
sendMessageToClients({ Message: "Error: Config details not found!" });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const admins = await _sync.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
const data = await _sync.getItem(undefined,userid); //getting all root folders aka libraries
|
||||
|
||||
const existingIds = await db
|
||||
.query('SELECT "Id" FROM jf_libraries')
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
dataToInsert = await data.map(jf_libraries_mapping);
|
||||
} else {
|
||||
dataToInsert = await data.filter((row) => !existingIds.includes(row.Id)).map(jf_libraries_mapping);
|
||||
}
|
||||
|
||||
if (dataToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_libraries",dataToInsert,jf_libraries_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
if (toDeleteIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_libraries",toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(toDeleteIds.length + " Rows Removed.");
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
res.send();
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraries: `);
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////writeLibraryItems
|
||||
router.get("/writeLibraryItems", async (req, res) => {
|
||||
let message = [];
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
|
||||
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1' );
|
||||
const { rows: cleanup } = await db.query('DELETE FROM jf_playback_activity where "NowPlayingItemId" not in (select "Id" from jf_library_items)' );
|
||||
sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
|
||||
|
||||
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
return;
|
||||
@@ -237,20 +233,18 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/2" });
|
||||
|
||||
sendMessageToClients({
|
||||
color: "yellow",
|
||||
Message: "Beginning Library Item Sync",
|
||||
});
|
||||
//Get all Library items
|
||||
//gets all libraries
|
||||
const libraries = await _sync.getItem();
|
||||
sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",});
|
||||
|
||||
const admins = await _sync.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
const libraries = await _sync.getItem(undefined,userid);
|
||||
const data = [];
|
||||
let insertCounter = 0;
|
||||
let deleteCounter = 0;
|
||||
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
|
||||
for (let i = 0; i < libraries.length; i++) {
|
||||
const item = libraries[i];
|
||||
let libraryItems = await _sync.getItem(item.Id);
|
||||
let libraryItems = await _sync.getItem(item.Id,userid);
|
||||
const libraryItemsWithParent = libraryItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: item.Id },
|
||||
@@ -258,153 +252,79 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
data.push(...libraryItemsWithParent);
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
|
||||
const existingIds = await db
|
||||
.query('SELECT "Id" FROM jf_library_items')
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
//data mapping
|
||||
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
dataToInsert = await data.map(jf_library_items_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
dataToInsert = await data
|
||||
.filter((row) => !existingIds.includes(row.Id))
|
||||
.map(jf_library_items_mapping);
|
||||
}
|
||||
|
||||
//Bulkinsert new data not on db
|
||||
|
||||
if (dataToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
dataToInsert,
|
||||
jf_library_items_columns,
|
||||
"jf_library_items"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: dataToInsert.length + " Rows Inserted.",
|
||||
});
|
||||
insertCounter += dataToInsert.length;
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing Item insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk insert" });
|
||||
let result = await db.insertBulk("jf_library_items",dataToInsert,jf_library_items_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertCounter += dataToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIds.length > data.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const AllIds = data.map((row) => row.Id);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_library_items WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: existingIds.length - data.length + " Rows Removed.",
|
||||
});
|
||||
deleteCounter += existingIds.length - data.length;
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing Item removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk delete" });
|
||||
// sendMessageToClients({Message:"No new Library items to bulk delete"});
|
||||
}
|
||||
//Sent logs
|
||||
|
||||
sendMessageToClients({
|
||||
color: "dodgerblue",
|
||||
Message: insertCounter + " Library Items Inserted.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "orange",
|
||||
Message: deleteCounter + " Library Items Removed.",
|
||||
});
|
||||
|
||||
const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
if (toDeleteIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_library_items",toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteCounter +=toDeleteIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
}
|
||||
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",});
|
||||
sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
|
||||
|
||||
res.send(message);
|
||||
res.send();
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraryItems: `);
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////writeSeasonsAndEpisodes
|
||||
router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/2" });
|
||||
sendMessageToClients({
|
||||
color: "yellow",
|
||||
Message: "Beginning Seasons and Episode sync",
|
||||
});
|
||||
const message = [];
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",});
|
||||
|
||||
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
|
||||
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
const { rows: shows } = await db.query(
|
||||
`SELECT * FROM public.jf_library_items where "Type"='Series'`
|
||||
);
|
||||
const { rows: shows } = await db.query(`SELECT * FROM public.jf_library_items where "Type"='Series'`);
|
||||
|
||||
let insertSeasonsCount = 0;
|
||||
let insertEpisodeCount = 0;
|
||||
let deleteSeasonsCount = 0;
|
||||
let deleteEpisodeCount = 0;
|
||||
|
||||
const admins = await _sync.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
//loop for each show
|
||||
for (const show of shows) {
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id);
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id,userid);
|
||||
|
||||
//
|
||||
//get existing seasons and episodes
|
||||
console.log(show.Id);
|
||||
const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
let existingIdsEpisodes = [];
|
||||
@@ -448,202 +368,63 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
if (seasonsToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_library_seasons",seasonsToInsert,jf_library_seasons_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: seasonsToInsert.length + " Rows Inserted for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
insertSeasonsCount += seasonsToInsert.length;
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk insert for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
const toDeleteIds = existingIdsSeasons.filter((id) =>!data.allSeasons.some((row) => row.Id === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (toDeleteIds.length > 0) {
|
||||
|
||||
|
||||
let table="jf_library_seasons";
|
||||
let result = await db.deleteBulk(table,toDeleteIds);
|
||||
let result = await db.deleteBulk("jf_library_seasons",toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: toDeleteIds.length + " Rows Removed for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: table,
|
||||
});
|
||||
deleteSeasonsCount +=toDeleteIds.length;
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: result.message,
|
||||
ItemId: show.Id,
|
||||
TableName: table,
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: result.message,
|
||||
});
|
||||
|
||||
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk delete for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
// sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
}
|
||||
}
|
||||
//insert delete episodes
|
||||
//Bulkinsert new data not on db
|
||||
if (episodesToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
let result = await db.insertBulk("jf_library_episodes",episodesToInsert,jf_library_episodes_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertEpisodeCount += episodesToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
episodesToInsert,
|
||||
jf_library_episodes_columns,
|
||||
"jf_library_episodes"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message:
|
||||
episodesToInsert.length + " Rows Inserted for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
insertEpisodeCount += episodesToInsert.length;
|
||||
// sendMessageToClients({color:'dodgerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk insert for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
// sendMessageToClients({Message:"No new data to bulk insert for " + show.Name});
|
||||
}
|
||||
const toDeleteEpisodeIds = existingIdsEpisodes.filter((id) =>!data.allEpisodes.some((row) => (row.Id + row.ParentId) === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIdsEpisodes.length > data.allEpisodes.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
if (toDeleteEpisodeIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_library_episodes",toDeleteEpisodeIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteEpisodeCount +=toDeleteEpisodeIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const AllIds = data.allEpisodes.map((row) => row.Id + row.ParentId);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_library_episodes WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message:
|
||||
existingIdsEpisodes.length -
|
||||
data.allEpisodes.length +
|
||||
" Rows Removed for " +
|
||||
show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
deleteEpisodeCount +=
|
||||
existingIdsEpisodes.length - data.allEpisodes.length;
|
||||
// sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk delete for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
// sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
}
|
||||
|
||||
|
||||
sendMessageToClients({ Message: "Sync complete for " + show.Name });
|
||||
}
|
||||
|
||||
sendMessageToClients({
|
||||
color: "dodgerblue",
|
||||
Message: insertSeasonsCount + " Seasons inserted.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "orange",
|
||||
Message: deleteSeasonsCount + " Seasons Removed.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "dodgerblue",
|
||||
Message: insertEpisodeCount + " Episodes inserted.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "orange",
|
||||
Message: deleteEpisodeCount + " Episodes Removed.",
|
||||
});
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
|
||||
res.send(message);
|
||||
res.send();
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `);
|
||||
});
|
||||
|
||||
//////////////////////////////////////
|
||||
|
||||
@@ -10,6 +10,10 @@ async function ActivityMonitor(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;
|
||||
|
||||
|
||||
2
postgres-image/Dockerfile
Normal file
2
postgres-image/Dockerfile
Normal file
@@ -0,0 +1,2 @@
|
||||
FROM postgres
|
||||
COPY init.sql /docker-entrypoint-initdb.d/
|
||||
3
postgres-image/commands.txt
Normal file
3
postgres-image/commands.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
docker run --name jellystat-postgres -e POSTGRES_PASSWORD=fy1W0POt5$ -p 25432:5432 -d jellystat-postgres
|
||||
|
||||
docker build -t jellystat-postgres .
|
||||
591
postgres-image/init.sql
Normal file
591
postgres-image/init.sql
Normal file
@@ -0,0 +1,591 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 15.2 (Debian 15.2-1.pgdg110+1)
|
||||
-- Dumped by pg_dump version 15.1
|
||||
|
||||
-- Started on 2023-03-21 19:12:22 UTC
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- TOC entry 3389 (class 1262 OID 16387)
|
||||
-- Name: jfstat; Type: DATABASE; Schema: -; Owner: jfstat
|
||||
--
|
||||
|
||||
CREATE DATABASE jfstat WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE_PROVIDER = libc LOCALE = 'en_US.utf8';
|
||||
|
||||
|
||||
-- ALTER DATABASE jfstat OWNER TO jfstat;
|
||||
|
||||
\connect jfstat
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
--
|
||||
-- TOC entry 232 (class 1255 OID 41783)
|
||||
-- Name: fs_most_active_user(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_most_active_user(days integer) RETURNS TABLE("Plays" bigint, "UserId" text, "Name" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT count(*) AS "Plays",
|
||||
jf_playback_activity."UserId",
|
||||
jf_playback_activity."UserName" AS "Name"
|
||||
FROM jf_playback_activity
|
||||
WHERE jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) AND NOW()
|
||||
GROUP BY jf_playback_activity."UserId", jf_playback_activity."UserName"
|
||||
ORDER BY (count(*)) DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_most_active_user(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 246 (class 1255 OID 41695)
|
||||
-- Name: fs_most_played_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_most_played_items(days integer, itemtype text) RETURNS TABLE("Plays" bigint, total_playback_duration numeric, "Name" text, "Id" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.plays,
|
||||
t.total_playback_duration,
|
||||
i."Name",
|
||||
i."Id"
|
||||
FROM (
|
||||
SELECT
|
||||
count(*) AS plays,
|
||||
sum(jf_playback_activity."PlaybackDuration") AS total_playback_duration,
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
FROM
|
||||
jf_playback_activity
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
GROUP BY
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY
|
||||
count(*) DESC
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON t."NowPlayingItemId" = i."Id"
|
||||
AND i."Type" = itemType
|
||||
ORDER BY
|
||||
t.plays DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_most_played_items(days integer, itemtype text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 245 (class 1255 OID 41690)
|
||||
-- Name: fs_most_popular_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_most_popular_items(days integer, itemtype text) RETURNS TABLE(unique_viewers bigint, latest_activity_date timestamp with time zone, "Name" text, "Id" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.unique_viewers,
|
||||
t.latest_activity_date,
|
||||
i."Name",
|
||||
i."Id"
|
||||
FROM (
|
||||
SELECT
|
||||
jf_playback_activity."NowPlayingItemId",
|
||||
count(DISTINCT jf_playback_activity."UserId") AS unique_viewers,
|
||||
latest_activity_date.latest_date AS latest_activity_date
|
||||
FROM
|
||||
jf_playback_activity
|
||||
JOIN (
|
||||
SELECT
|
||||
jf_playback_activity_1."NowPlayingItemId",
|
||||
max(jf_playback_activity_1."ActivityDateInserted") AS latest_date
|
||||
FROM
|
||||
jf_playback_activity jf_playback_activity_1
|
||||
GROUP BY jf_playback_activity_1."NowPlayingItemId"
|
||||
) latest_activity_date
|
||||
ON jf_playback_activity."NowPlayingItemId" = latest_activity_date."NowPlayingItemId"
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
GROUP BY
|
||||
jf_playback_activity."NowPlayingItemId", latest_activity_date.latest_date
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON t."NowPlayingItemId" = i."Id"
|
||||
AND i."Type" = itemType
|
||||
ORDER BY
|
||||
t.unique_viewers DESC, t.latest_activity_date DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_most_popular_items(days integer, itemtype text) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 231 (class 1255 OID 41730)
|
||||
-- Name: fs_most_used_clients(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_most_used_clients(days integer) RETURNS TABLE("Plays" bigint, "Client" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT count(*) AS "Plays",
|
||||
jf_playback_activity."Client"
|
||||
FROM jf_playback_activity
|
||||
WHERE jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) AND NOW()
|
||||
GROUP BY jf_playback_activity."Client"
|
||||
ORDER BY (count(*)) DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_most_used_clients(days integer) OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 244 (class 1255 OID 41701)
|
||||
-- Name: fs_most_viewed_libraries(integer); Type: FUNCTION; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE FUNCTION public.fs_most_viewed_libraries(days integer) RETURNS TABLE("Plays" numeric, "Id" text, "Name" text, "ServerId" text, "IsFolder" boolean, "Type" text, "CollectionType" text, "ImageTagsPrimary" text)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
sum(t."Plays"),
|
||||
l."Id",
|
||||
l."Name",
|
||||
l."ServerId",
|
||||
l."IsFolder",
|
||||
l."Type",
|
||||
l."CollectionType",
|
||||
l."ImageTagsPrimary"
|
||||
FROM (
|
||||
SELECT count(*) AS "Plays",
|
||||
sum(jf_playback_activity."PlaybackDuration") AS "TotalPlaybackDuration",
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
FROM jf_playback_activity
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
|
||||
GROUP BY jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY "Plays" DESC
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON i."Id" = t."NowPlayingItemId"
|
||||
JOIN jf_libraries l
|
||||
ON l."Id" = i."ParentId"
|
||||
GROUP BY
|
||||
l."Id"
|
||||
ORDER BY
|
||||
(sum( t."Plays")) DESC;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
ALTER FUNCTION public.fs_most_viewed_libraries(days integer) OWNER TO postgres;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- TOC entry 220 (class 1259 OID 16395)
|
||||
-- Name: app_config; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.app_config (
|
||||
"ID" integer NOT NULL,
|
||||
"JF_HOST" text,
|
||||
"JF_API_KEY" text,
|
||||
"APP_USER" text,
|
||||
"APP_PASSWORD" text
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.app_config OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 221 (class 1259 OID 16402)
|
||||
-- Name: app_config_ID_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE public.app_config ALTER COLUMN "ID" ADD GENERATED ALWAYS AS IDENTITY (
|
||||
SEQUENCE NAME public."app_config_ID_seq"
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 228 (class 1259 OID 41300)
|
||||
-- Name: jf_activity_watchdog; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_activity_watchdog (
|
||||
"Id" text NOT NULL,
|
||||
"IsPaused" boolean DEFAULT false,
|
||||
"UserId" text,
|
||||
"UserName" text,
|
||||
"Client" text,
|
||||
"DeviceName" text,
|
||||
"DeviceId" text,
|
||||
"ApplicationVersion" text,
|
||||
"NowPlayingItemId" text,
|
||||
"NowPlayingItemName" text,
|
||||
"SeasonId" text,
|
||||
"SeriesName" text,
|
||||
"EpisodeId" text,
|
||||
"PlaybackDuration" bigint,
|
||||
"ActivityDateInserted" timestamp with time zone,
|
||||
"PlayMethod" text
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_activity_watchdog OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 227 (class 1259 OID 41294)
|
||||
-- Name: jf_playback_activity; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_playback_activity (
|
||||
"Id" text NOT NULL,
|
||||
"IsPaused" boolean DEFAULT false,
|
||||
"UserId" text,
|
||||
"UserName" text,
|
||||
"Client" text,
|
||||
"DeviceName" text,
|
||||
"DeviceId" text,
|
||||
"ApplicationVersion" text,
|
||||
"NowPlayingItemId" text,
|
||||
"NowPlayingItemName" text,
|
||||
"SeasonId" text,
|
||||
"SeriesName" text,
|
||||
"EpisodeId" text,
|
||||
"PlaybackDuration" bigint,
|
||||
"ActivityDateInserted" timestamp with time zone,
|
||||
"PlayMethod" text
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_playback_activity OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 229 (class 1259 OID 41731)
|
||||
-- Name: jf_users; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_users (
|
||||
"Id" text NOT NULL,
|
||||
"Name" text,
|
||||
"PrimaryImageTag" text,
|
||||
"LastLoginDate" timestamp with time zone,
|
||||
"LastActivityDate" timestamp with time zone,
|
||||
"IsAdministrator" boolean
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_users OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 230 (class 1259 OID 41771)
|
||||
-- Name: jf_all_user_activity; Type: VIEW; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE VIEW public.jf_all_user_activity AS
|
||||
SELECT u."Id" AS "UserId",
|
||||
u."PrimaryImageTag",
|
||||
u."Name" AS "UserName",
|
||||
CASE
|
||||
WHEN (j."SeriesName" IS NULL) THEN j."NowPlayingItemName"
|
||||
ELSE ((j."SeriesName" || ' - '::text) || j."NowPlayingItemName")
|
||||
END AS "LastWatched",
|
||||
j."ActivityDateInserted" AS "LastActivityDate",
|
||||
((j."Client" || ' - '::text) || j."DeviceName") AS "LastClient",
|
||||
plays."TotalPlays",
|
||||
plays."TotalWatchTime",
|
||||
(now() - j."ActivityDateInserted") AS "LastSeen"
|
||||
FROM ((( SELECT jf_users."Id",
|
||||
jf_users."Name",
|
||||
jf_users."PrimaryImageTag",
|
||||
jf_users."LastLoginDate",
|
||||
jf_users."LastActivityDate",
|
||||
jf_users."IsAdministrator"
|
||||
FROM public.jf_users) u
|
||||
LEFT JOIN LATERAL ( 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"
|
||||
FROM public.jf_playback_activity
|
||||
WHERE (jf_playback_activity."UserId" = u."Id")
|
||||
ORDER BY jf_playback_activity."ActivityDateInserted" DESC
|
||||
LIMIT 1) j ON (true))
|
||||
LEFT JOIN LATERAL ( SELECT count(*) AS "TotalPlays",
|
||||
sum(jf_playback_activity."PlaybackDuration") AS "TotalWatchTime"
|
||||
FROM public.jf_playback_activity
|
||||
WHERE (jf_playback_activity."UserId" = u."Id")) plays ON (true))
|
||||
ORDER BY j."ActivityDateInserted";
|
||||
|
||||
|
||||
ALTER TABLE public.jf_all_user_activity OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 222 (class 1259 OID 16411)
|
||||
-- Name: jf_libraries; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_libraries (
|
||||
"Id" text NOT NULL,
|
||||
"Name" text NOT NULL,
|
||||
"ServerId" text,
|
||||
"IsFolder" boolean DEFAULT true NOT NULL,
|
||||
"Type" text DEFAULT 'CollectionFolder'::text NOT NULL,
|
||||
"CollectionType" text NOT NULL,
|
||||
"ImageTagsPrimary" text
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_libraries OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 226 (class 1259 OID 25160)
|
||||
-- Name: jf_library_count_view; Type: VIEW; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE VIEW public.jf_library_count_view AS
|
||||
SELECT
|
||||
NULL::text AS "Id",
|
||||
NULL::text AS "Name",
|
||||
NULL::text AS "CollectionType",
|
||||
NULL::bigint AS "Library_Count",
|
||||
NULL::bigint AS "Season_Count",
|
||||
NULL::bigint AS "Episode_Count";
|
||||
|
||||
|
||||
ALTER TABLE public.jf_library_count_view OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 225 (class 1259 OID 24906)
|
||||
-- Name: jf_library_episodes; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_library_episodes (
|
||||
"Id" text NOT NULL,
|
||||
"EpisodeId" text NOT NULL,
|
||||
"Name" text,
|
||||
"ServerId" text,
|
||||
"PremiereDate" timestamp with time zone,
|
||||
"OfficialRating" text,
|
||||
"CommunityRating" double precision,
|
||||
"RunTimeTicks" bigint,
|
||||
"ProductionYear" integer,
|
||||
"IndexNumber" integer,
|
||||
"ParentIndexNumber" integer,
|
||||
"Type" text,
|
||||
"ParentLogoItemId" text,
|
||||
"ParentBackdropItemId" text,
|
||||
"ParentBackdropImageTags" text,
|
||||
"SeriesId" text,
|
||||
"SeasonId" text,
|
||||
"SeasonName" text,
|
||||
"SeriesName" text
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_library_episodes OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 223 (class 1259 OID 24599)
|
||||
-- Name: jf_library_items; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_library_items (
|
||||
"Id" text NOT NULL,
|
||||
"Name" text NOT NULL,
|
||||
"ServerId" text,
|
||||
"PremiereDate" timestamp with time zone,
|
||||
"EndDate" timestamp with time zone,
|
||||
"CommunityRating" double precision,
|
||||
"RunTimeTicks" bigint,
|
||||
"ProductionYear" integer,
|
||||
"IsFolder" boolean,
|
||||
"Type" text,
|
||||
"Status" text,
|
||||
"ImageTagsPrimary" text,
|
||||
"ImageTagsBanner" text,
|
||||
"ImageTagsLogo" text,
|
||||
"ImageTagsThumb" text,
|
||||
"BackdropImageTags" text,
|
||||
"ParentId" text NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_library_items OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 224 (class 1259 OID 24731)
|
||||
-- Name: jf_library_seasons; Type: TABLE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE TABLE public.jf_library_seasons (
|
||||
"Id" text NOT NULL,
|
||||
"Name" text,
|
||||
"ServerId" text,
|
||||
"IndexNumber" integer,
|
||||
"Type" text,
|
||||
"ParentLogoItemId" text,
|
||||
"ParentBackdropItemId" text,
|
||||
"ParentBackdropImageTags" text,
|
||||
"SeriesName" text,
|
||||
"SeriesId" text,
|
||||
"SeriesPrimaryImageTag" text
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.jf_library_seasons OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- TOC entry 3228 (class 2606 OID 16401)
|
||||
-- Name: app_config app_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.app_config
|
||||
ADD CONSTRAINT app_config_pkey PRIMARY KEY ("ID");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3230 (class 2606 OID 16419)
|
||||
-- Name: jf_libraries jf_libraries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.jf_libraries
|
||||
ADD CONSTRAINT jf_libraries_pkey PRIMARY KEY ("Id");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3236 (class 2606 OID 24912)
|
||||
-- Name: jf_library_episodes jf_library_episodes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.jf_library_episodes
|
||||
ADD CONSTRAINT jf_library_episodes_pkey PRIMARY KEY ("Id");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3232 (class 2606 OID 24605)
|
||||
-- Name: jf_library_items jf_library_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.jf_library_items
|
||||
ADD CONSTRAINT jf_library_items_pkey PRIMARY KEY ("Id");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3234 (class 2606 OID 24737)
|
||||
-- Name: jf_library_seasons jf_library_seasons_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.jf_library_seasons
|
||||
ADD CONSTRAINT jf_library_seasons_pkey PRIMARY KEY ("Id");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3238 (class 2606 OID 41737)
|
||||
-- Name: jf_users jf_users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.jf_users
|
||||
ADD CONSTRAINT jf_users_pkey PRIMARY KEY ("Id");
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3382 (class 2618 OID 25163)
|
||||
-- Name: jf_library_count_view _RETURN; Type: RULE; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
CREATE OR REPLACE VIEW public.jf_library_count_view AS
|
||||
SELECT l."Id",
|
||||
l."Name",
|
||||
l."CollectionType",
|
||||
count(DISTINCT i."Id") AS "Library_Count",
|
||||
count(DISTINCT s."Id") AS "Season_Count",
|
||||
count(DISTINCT e."Id") AS "Episode_Count"
|
||||
FROM (((public.jf_libraries l
|
||||
JOIN public.jf_library_items i ON ((i."ParentId" = l."Id")))
|
||||
LEFT JOIN public.jf_library_seasons s ON ((s."SeriesId" = i."Id")))
|
||||
LEFT JOIN public.jf_library_episodes e ON ((e."SeasonId" = s."Id")))
|
||||
GROUP BY l."Id", l."Name"
|
||||
ORDER BY (count(DISTINCT i."Id")) DESC;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3239 (class 2606 OID 24617)
|
||||
-- Name: jf_library_items jf_library_items_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.jf_library_items
|
||||
ADD CONSTRAINT jf_library_items_fkey FOREIGN KEY ("ParentId") REFERENCES public.jf_libraries("Id") ON DELETE SET NULL NOT VALID;
|
||||
|
||||
|
||||
--
|
||||
-- TOC entry 3390 (class 0 OID 0)
|
||||
-- Dependencies: 3239
|
||||
-- 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-21 19:12:22 UTC
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>JellyStat</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "JellyStat",
|
||||
"name": "Statistics for Jellyfin",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
||||
12
src/App.css
12
src/App.css
@@ -1,11 +1,7 @@
|
||||
.App {
|
||||
/* text-align: center; */
|
||||
/* padding-bottom: 5%; */
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
/* grid-template-areas:
|
||||
"sidenav main"; */
|
||||
grid-template-columns: auto 1fr;
|
||||
|
||||
|
||||
main{
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
|
||||
26
src/App.js
26
src/App.js
@@ -13,21 +13,23 @@ import Loading from './pages/components/loading';
|
||||
import Setup from './pages/setup';
|
||||
|
||||
|
||||
import SideNav from './pages/components/sidenav';
|
||||
import Navbar from './pages/components/navbar';
|
||||
import Home from './pages/home';
|
||||
import Settings from './pages/settings';
|
||||
import Activity from './pages/activity';
|
||||
import UserActivity from './pages/useractivity';
|
||||
import Users from './pages/users';
|
||||
import UserInfo from './pages/user-info';
|
||||
import Libraries from './pages/libraries';
|
||||
import ErrorPage from './pages/components/error';
|
||||
|
||||
import RecentlyPlayed from './pages/components/recentlyplayed';
|
||||
|
||||
import UserData from './pages/userdata';
|
||||
import Testing from './pages/testing';
|
||||
|
||||
function App() {
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [errorFlag, seterrorFlag] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -37,8 +39,10 @@ function App() {
|
||||
const newConfig = await Config();
|
||||
if(newConfig !== 'ERR_NETWORK'){
|
||||
setConfig(newConfig);
|
||||
setLoading(false);
|
||||
}else{
|
||||
seterrorFlag(true);
|
||||
}
|
||||
setLoading(false);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -55,6 +59,10 @@ if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (errorFlag) {
|
||||
return <ErrorPage message={"Error: Unable to connect to Jellystat Backend"} />;
|
||||
}
|
||||
|
||||
if (!config || config.apiKey ==null) {
|
||||
return <Setup />;
|
||||
}
|
||||
@@ -63,16 +71,16 @@ if (!config || config.apiKey ==null) {
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<SideNav />
|
||||
<Navbar />
|
||||
<div>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/activity" element={<Activity />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route path="/user-info/:UserId" element={<UserInfo />} />
|
||||
<Route path="/libraries" element={<Libraries />} />
|
||||
<Route path="/usersactivity" element={<UserActivity />} />
|
||||
<Route path="/userdata" element={<UserData />} />
|
||||
<Route path="/testing" element={<Testing />} />
|
||||
<Route path="/recent" element={<RecentlyPlayed />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -17,3 +17,25 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
overflow: auto; /* show scrollbar when needed */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar {
|
||||
width: 10px; /* set scrollbar width */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* set track color */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb {
|
||||
background-color: #8888884d; /* set thumb color */
|
||||
border-radius: 5px; /* round corners */
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #88888883; /* set thumb color */
|
||||
}
|
||||
@@ -18,36 +18,24 @@ export const navData = [
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
icon: <FileListFillIcon />,
|
||||
text: "Activity",
|
||||
link: "activity"
|
||||
icon: <UserFillIcon />,
|
||||
text: "Users",
|
||||
link: "users"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: <GalleryFillIcon />,
|
||||
text: "Libraries",
|
||||
link: "libraries"
|
||||
} ,
|
||||
{
|
||||
id: 3,
|
||||
icon: <UserFillIcon />,
|
||||
text: "Recently Played",
|
||||
link: "recent"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: <BarChartFillIcon />,
|
||||
text: "User Activity",
|
||||
link: "usersactivity"
|
||||
icon: <ReactjsFillIcon />,
|
||||
text: "Component Testing Playground",
|
||||
link: "testing"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
icon: <ReactjsFillIcon />,
|
||||
text: "Library Data",
|
||||
link: "userdata"
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
icon: <SettingsFillIcon />,
|
||||
text: "Settings",
|
||||
link: "settings"
|
||||
|
||||
@@ -7,9 +7,12 @@ import MostUsedClient from "./statCards/most_used_client";
|
||||
import MostActiveUsers from "./statCards/most_active_users";
|
||||
import MPSeries from "./statCards/mp_series";
|
||||
import MPMovies from "./statCards/mp_movies";
|
||||
import MVMusic from "./statCards/mv_music";
|
||||
import MPMusic from "./statCards/mp_music";
|
||||
|
||||
import "../css/statCard.css";
|
||||
|
||||
function StatCards() {
|
||||
function WatchStatistics() {
|
||||
const [days, setDays] = useState(30);
|
||||
const [input, setInput] = useState(30);
|
||||
|
||||
@@ -50,12 +53,15 @@ function StatCards() {
|
||||
<MPMovies days={days} />
|
||||
<MVSeries days={days} />
|
||||
<MPSeries days={days} />
|
||||
<MVMusic days={days}/>
|
||||
<MPMusic days={days}/>
|
||||
<MVLibraries days={days} />
|
||||
<MostUsedClient days={days} />
|
||||
<MostActiveUsers days={days} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatCards;
|
||||
export default WatchStatistics;
|
||||
12
src/pages/components/error.js
Normal file
12
src/pages/components/error.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import "../css/error.css";
|
||||
|
||||
function ErrorPage(props) {
|
||||
return (
|
||||
<div className="error">
|
||||
<div className="message">{props.message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorPage;
|
||||
@@ -1,27 +1,21 @@
|
||||
import "../css/libraryOverview.css";
|
||||
import Config from "../../lib/config";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Loading from "./loading";
|
||||
|
||||
import LibraryStatComponent from "./libraryStatCard/library-stat-component";
|
||||
|
||||
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
|
||||
export default function LibraryOverView() {
|
||||
const SeriesIcon=<TvLineIcon size={"80%"} /> ;
|
||||
const MovieIcon=<FilmLineIcon size={"80%"} /> ;
|
||||
const [data, setData] = useState([]);
|
||||
const [base_url, setURL] = useState("");
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (base_url === "") {
|
||||
Config()
|
||||
.then((config) => {
|
||||
setURL(config.hostUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
const fetchData = () => {
|
||||
const url = `/stats/getLibraryOverview`;
|
||||
axios
|
||||
@@ -33,7 +27,7 @@ export default function LibraryOverView() {
|
||||
if (!data || data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
}, [data, base_url]);
|
||||
}, [data]);
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Loading />;
|
||||
@@ -43,67 +37,12 @@ export default function LibraryOverView() {
|
||||
<div>
|
||||
<h1>Library Statistics</h1>
|
||||
<div className="overview-container">
|
||||
<div className="library-card">
|
||||
|
||||
<div className="library-image">
|
||||
<div className="library-icons">
|
||||
<TvLineIcon size={"80%"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="library">
|
||||
<div className="library-header">
|
||||
<div>MOVIE LIBRARIES</div>
|
||||
<div className="library-header-count">MOVIES</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-list">
|
||||
{data &&
|
||||
data.filter((stat) => stat.CollectionType === "movies")
|
||||
.map((item, index) => (
|
||||
<div className="library-item" key={item.Id}>
|
||||
<p className="library-item-index">{index + 1}</p>
|
||||
<p className="library-item-name">{item.Name}</p>
|
||||
<p className="library-item-count">{item.Library_Count}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="library-card">
|
||||
|
||||
<div className="library-image">
|
||||
<div className="library-icons">
|
||||
<FilmLineIcon size={"80%"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="library">
|
||||
<div className="library-header">
|
||||
<div>SHOW LIBRARIES</div>
|
||||
<div className="library-header-count">
|
||||
SERIES / SEASONS / EPISODES
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-list">
|
||||
{data &&
|
||||
data.filter((stat) => stat.CollectionType === "tvshows")
|
||||
.map((item, index) => (
|
||||
<div className="library-item" key={item.Id}>
|
||||
<p className="library-item-index">{index + 1}</p>
|
||||
<p className="library-item-name">{item.Name}</p>
|
||||
<p className="library-item-count">{item.Library_Count} / {item.Season_Count} / {item.Episode_Count}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<LibraryStatComponent data={data.filter((stat) => stat.CollectionType === "movies")} heading={"MOVIE LIBRARIES"} units={"MOVIES"} icon={MovieIcon}/>
|
||||
<LibraryStatComponent data={data.filter((stat) => stat.CollectionType === "tvshows")} heading={"SHOW LIBRARIES"} units={"SERIES / SEASONS / EPISODES"} icon={SeriesIcon}/>
|
||||
<LibraryStatComponent data={data.filter((stat) => stat.CollectionType === "music")} heading={"MUSIC LIBRARIES"} units={"SONGS"} icon={SeriesIcon}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
|
||||
function LibraryStatComponent(props) {
|
||||
|
||||
if (props.data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="library-card">
|
||||
|
||||
<div className="library-image">
|
||||
<div className="library-icons">
|
||||
{props.icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="library">
|
||||
<div className="library-header">
|
||||
<div>{props.heading}</div>
|
||||
<div className="library-header-count">{props.units}</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-list">
|
||||
{props.data
|
||||
.map((item, index) => (
|
||||
<div className="library-item" key={item.Id}>
|
||||
<p className="library-item-index">{index + 1}</p>
|
||||
<p className="library-item-name">{item.Name}</p>
|
||||
<p className="library-item-count">{item.CollectionType =='tvshows'? (item.Library_Count+' / '+item.Season_Count+' / '+item.Episode_Count): item.Library_Count}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LibraryStatComponent;
|
||||
19
src/pages/components/navbar.js
Normal file
19
src/pages/components/navbar.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { navData } from "../../lib/navdata";
|
||||
import "../css/navbar.css"
|
||||
|
||||
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<div className={'navbar'}>
|
||||
{navData.map(item =>{
|
||||
return <NavLink key={item.id} className={'navitem'} to={item.link}>
|
||||
{item.icon}
|
||||
<span className={'nav-text'}>{item.text}</span>
|
||||
</NavLink>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import API from "../../classes/jellyfin-api";
|
||||
import "../css/sessions.css";
|
||||
// import "../../App.css"
|
||||
|
||||
import SessionCard from "./session-card";
|
||||
import SessionCard from "./sessions/session-card";
|
||||
|
||||
import Loading from "./loading";
|
||||
|
||||
@@ -44,7 +44,8 @@ function Sessions() {
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return(<div>
|
||||
return(
|
||||
<div className="sessions">
|
||||
<h1>Sessions</h1>
|
||||
<div style={{color:"grey", fontSize:"0.8em", fontStyle:"italic"}}>
|
||||
No Active Sessions Found
|
||||
@@ -53,9 +54,9 @@ function Sessions() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="sessions">
|
||||
<h1>Sessions</h1>
|
||||
<div className="sessions">
|
||||
<div className="sessions-container">
|
||||
{data &&
|
||||
data
|
||||
.sort((a, b) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import '../../css/websocket/websocket.css';
|
||||
|
||||
const WebSocketComponent = () => {
|
||||
const TerminalComponent = () => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
@@ -46,4 +46,4 @@ const WebSocketComponent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketComponent;
|
||||
export default TerminalComponent;
|
||||
@@ -1,17 +1,27 @@
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
// import Config from "../../../lib/config";
|
||||
// import Loading from "../loading";
|
||||
|
||||
|
||||
import "../../css/settings.css";
|
||||
|
||||
export default function LibrarySync() {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
async function writeSeasonsAndEpisodes() {
|
||||
async function beginSync() {
|
||||
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
await axios
|
||||
.get("/sync/writeLibraries")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
await axios
|
||||
.get("/sync/writeLibraryItems")
|
||||
.then((response) => {
|
||||
@@ -34,13 +44,25 @@ export default function LibrarySync() {
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
|
||||
await axios
|
||||
.get("/sync/writeUsers")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
setProcessing(false);
|
||||
// return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
writeSeasonsAndEpisodes();
|
||||
beginSync();
|
||||
console.log('Button clicked!');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import "../css/sidenav.css"
|
||||
import { NavLink } from "react-router-dom";
|
||||
|
||||
import MenuUnfoldFillIcon from 'remixicon-react/MenuUnfoldFillIcon';
|
||||
import MenuFoldFillIcon from 'remixicon-react/MenuFoldFillIcon';
|
||||
|
||||
import { navData } from "../../lib/navdata";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Sidenav() {
|
||||
const [open, setopen] = useState(false)
|
||||
const toggleOpen = () => {
|
||||
setopen(!open)
|
||||
}
|
||||
return (
|
||||
<div className={open? 'sidenav':'sidenav Closed'}>
|
||||
<button className={open? 'menuBtn menuBtn-open':'menuBtn' } onClick={toggleOpen}>
|
||||
{open? <MenuFoldFillIcon color="#fff" />:<MenuUnfoldFillIcon color="#fff" />}
|
||||
</button>
|
||||
{navData.map(item =>{
|
||||
return <NavLink key={item.id} className={'sideitem'} to={item.link}>
|
||||
{item.icon}
|
||||
<span className={open? 'text-open' :'text-closed'}>{open? item.text : ''}</span>
|
||||
</NavLink>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
|
||||
function MostActiveUsers() {
|
||||
function MostActiveUsers(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
@@ -31,7 +33,11 @@ function MostActiveUsers() {
|
||||
const url = `/stats/getMostActiveUsers`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
@@ -50,9 +56,15 @@ function MostActiveUsers() {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
|
||||
|
||||
@@ -95,30 +107,7 @@ function MostActiveUsers() {
|
||||
></img>
|
||||
}
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST ACTIVE USERS</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.UserId}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.UserName}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatComponent data={data} heading={"MOST ACTIVE USERS"} units={"Plays"}/>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,58 +1,49 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
import ComputerLineIcon from "remixicon-react/ComputerLineIcon";
|
||||
|
||||
function MostUsedClient() {
|
||||
function MostUsedClient(props) {
|
||||
const [data, setData] = useState([]);
|
||||
// const [base_url, setURL] = useState("");
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
const [days, setDays] = useState(30);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostUsedClient`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
}, [data, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
@@ -75,30 +66,7 @@ function MostUsedClient() {
|
||||
<ComputerLineIcon size={'80%'}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST USED CLIENTS</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Client}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Client}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatComponent data={data} heading={"MOST USED CLIENTS"} units={"Plays"}/>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
function MPMovies(props) {
|
||||
const [data, setData] = useState([]);
|
||||
@@ -98,30 +98,7 @@ function MPMovies(props) {
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST POPULAR MOVIES</div>
|
||||
<div className="stats-header-plays">Users</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.unique_viewers}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatComponent data={data} heading={"MOST POPULAR MOVIES"} units={"Users"}/>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
109
src/pages/components/statCards/mp_music.js
Normal file
109
src/pages/components/statCards/mp_music.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
function MPMusic(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
// const [base_url, setURL] = useState("");
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostPopularMusic`;
|
||||
|
||||
axios
|
||||
.post(url, { days: props.days }, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&quality=50"
|
||||
})`}}
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<img
|
||||
className="popular-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<StatComponent data={data} heading={"MOST POPULAR MUSIC"} units={"Users"}/>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MPMusic;
|
||||
@@ -3,6 +3,7 @@ import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
@@ -13,7 +14,6 @@ function MPSeries(props) {
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
console.log('PROPS: '+ days);
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
@@ -99,29 +99,7 @@ function MPSeries(props) {
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST POPULAR SERIES</div>
|
||||
<div className="stats-header-plays">Users</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.unique_viewers}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<StatComponent data={data} heading={"MOST POPULAR SERIES"} units={"Users"}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
@@ -11,23 +11,12 @@ import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
function MVLibraries(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostViewedLibraries`;
|
||||
|
||||
axios
|
||||
@@ -42,13 +31,9 @@ function MVLibraries(props) {
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
@@ -60,7 +45,7 @@ function MVLibraries(props) {
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
}, [data, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
@@ -89,29 +74,7 @@ function MVLibraries(props) {
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST VIEWED LIBRARIES</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<StatComponent data={data} heading={"MOST VIEWED LIBRARIES"} units={"Plays"}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,10 @@ import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
|
||||
function MVMovies(props) {
|
||||
function MVMusic(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
|
||||
@@ -98,33 +99,11 @@ function MVMovies(props) {
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST VIEWED MOVIES</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<StatComponent data={data} heading={"MOST VIEWED MOVIES"} units={"Plays"}/>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MVMovies;
|
||||
export default MVMusic;
|
||||
|
||||
109
src/pages/components/statCards/mv_music.js
Normal file
109
src/pages/components/statCards/mv_music.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
|
||||
function MVMovies(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostViewedMusic`;
|
||||
|
||||
axios
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&quality=50"
|
||||
})`}}
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<img
|
||||
className="popular-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<StatComponent data={data} heading={"MOST PLAYED MUSIC"} units={"Plays"}/>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MVMovies;
|
||||
@@ -4,6 +4,8 @@ import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
import StatComponent from "./statsComponent";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
function MVSeries(props) {
|
||||
@@ -98,29 +100,7 @@ function MVSeries(props) {
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST VIEWED SERIES</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<StatComponent data={data} heading={"MOST VIEWED SERIES"} units={"Plays"}/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
25
src/pages/components/statCards/statsComponent.js
Normal file
25
src/pages/components/statCards/statsComponent.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
function StatComponent(props) {
|
||||
return (
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
<div>{props.heading}</div>
|
||||
<div className="stats-header-plays">{props.units}</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-list">
|
||||
{props.data &&
|
||||
props.data.map((item, index) => (
|
||||
<div className="stat-item" key={item.Id}>
|
||||
<p className="stat-item-index">{index + 1}</p>
|
||||
<p className="stat-item-name">{item.Name || item.Client}</p>
|
||||
<p className="stat-item-count"> {item.Plays || item.unique_viewers}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatComponent;
|
||||
18
src/pages/css/error.css
Normal file
18
src/pages/css/error.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.error
|
||||
{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error .message
|
||||
{
|
||||
color:crimson;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
36
src/pages/css/navbar.css
Normal file
36
src/pages/css/navbar.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
background-color: rgb(10,25,41);
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
|
||||
.navitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
padding: 0 20px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.nav-text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.active
|
||||
{
|
||||
background-color: #308df046 !important;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.navitem:hover {
|
||||
background-color: #326aa541;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
.sessions {
|
||||
margin-bottom: 10px;
|
||||
|
||||
}
|
||||
|
||||
.sessions-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(520px, 520px));
|
||||
grid-auto-rows: 235px;/* max-width+offset so 215 + 20*/
|
||||
@@ -9,7 +14,6 @@
|
||||
|
||||
}
|
||||
|
||||
|
||||
.session-card {
|
||||
display: flex;
|
||||
color: white;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.sidenav {
|
||||
width: 250px;
|
||||
transition: width 0.3s ease-in-out;
|
||||
height: 100vh;
|
||||
/* z-index: 100; */
|
||||
top:0;
|
||||
background-color: rgb(10,25,41);
|
||||
/* padding-top: 28px; */
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.active
|
||||
{
|
||||
background-color: #308df046 !important;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
.Closed {
|
||||
/* composes: sidenav; */
|
||||
transition: width 0.3s ease-in-out;
|
||||
width: 60px;
|
||||
/* padding-top: 28px; */
|
||||
/* position: absolute; */
|
||||
|
||||
}
|
||||
.sideitem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
color: #B2BAC2;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.text-open {
|
||||
padding-left: 16px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in;
|
||||
}
|
||||
|
||||
.text-closed {
|
||||
/* padding-left: 16px; */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in;
|
||||
|
||||
}
|
||||
|
||||
.sideitem:hover {
|
||||
background-color: #244f7d1c;
|
||||
|
||||
}
|
||||
.menuBtn {
|
||||
align-self: center;
|
||||
align-self: flex-start;
|
||||
justify-self: flex-end;
|
||||
color: #B2BAC2;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding-left: 10px;
|
||||
width: inherit;
|
||||
transition: width 0.3s ease-in;
|
||||
|
||||
}
|
||||
|
||||
.menuBtn:hover {
|
||||
|
||||
background-color: #244f7d1c;
|
||||
|
||||
}
|
||||
|
||||
.menuBtn-open {
|
||||
width: 100%;
|
||||
transition: width 0.3s ease-in;
|
||||
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
.Heading
|
||||
{
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.Heading h1
|
||||
{
|
||||
padding-right: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-cards-container
|
||||
@@ -127,10 +129,11 @@
|
||||
height: 35px;
|
||||
color: white;
|
||||
display: flex;
|
||||
background-color: rgb(0, 99, 248,0.6);
|
||||
background-color: rgb(0, 0, 0,0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 1.2em;
|
||||
align-self: center;
|
||||
align-self: flex-end;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +168,6 @@ input[type=number] {
|
||||
.date-range .header,
|
||||
.date-range .trailer
|
||||
{
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
padding-inline: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
160
src/pages/css/users.css
Normal file
160
src/pages/css/users.css
Normal file
@@ -0,0 +1,160 @@
|
||||
.Users
|
||||
{
|
||||
color: white;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.user-activity-table {
|
||||
border-collapse: collapse;
|
||||
border-radius: 5px;
|
||||
/* margin: 25px 0; */
|
||||
font-size: 0.9em;
|
||||
font-family: sans-serif;
|
||||
min-width: 400px;
|
||||
/* box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); */
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
color: white;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
th,
|
||||
td
|
||||
{
|
||||
padding: 10px 15px;
|
||||
/* text-align: left; */
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-left:1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
td a{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
td:hover a{
|
||||
|
||||
color: rgb(0, 164, 219);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th:hover {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* tbody tr:last-of-type {
|
||||
border-bottom: 2px solid #009879;
|
||||
} */
|
||||
|
||||
|
||||
.card-user-image
|
||||
{
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
|
||||
}
|
||||
|
||||
tbody tr:hover
|
||||
{
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
|
||||
td:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
margin-inline: 5px;
|
||||
background-color: rgb(10,25,41);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
margin-inline: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.pagination-range
|
||||
{
|
||||
width: 130px;
|
||||
height: 35px;
|
||||
color: white;
|
||||
display: flex;
|
||||
background-color: rgb(0, 0, 0,0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 1.2em;
|
||||
align-self: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pagination-range select
|
||||
{
|
||||
|
||||
height: 35px;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(255, 255, 255, 0.1);
|
||||
color:white;
|
||||
font-size: 1em;
|
||||
|
||||
|
||||
}
|
||||
.pagination-range .header
|
||||
{
|
||||
padding-inline: 10px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
||||
select option {
|
||||
background-color: #4a4a4a;
|
||||
outline: unset;
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
}
|
||||
|
||||
.pagination-range .items
|
||||
{
|
||||
background-color: rgb(255, 255, 255, 0.1);
|
||||
padding-inline: 10px;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
.Users
|
||||
{
|
||||
color: white;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.user-activity-table {
|
||||
border-collapse: collapse;
|
||||
border-radius: 5px;
|
||||
/* margin: 25px 0; */
|
||||
font-size: 0.9em;
|
||||
font-family: sans-serif;
|
||||
min-width: 400px;
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
/* text-align: left; */
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-left:1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th:hover {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* tbody tr:last-of-type {
|
||||
border-bottom: 2px solid #009879;
|
||||
} */
|
||||
|
||||
|
||||
.card-user-image
|
||||
{
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
object-fit: cover;
|
||||
|
||||
}
|
||||
|
||||
tr:hover
|
||||
{
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
td:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
@@ -17,3 +17,25 @@
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
.console-container {
|
||||
overflow: auto; /* show scrollbar when needed */
|
||||
}
|
||||
|
||||
.console-container::-webkit-scrollbar {
|
||||
width: 10px; /* set scrollbar width */
|
||||
}
|
||||
|
||||
.console-container::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* set track color */
|
||||
}
|
||||
|
||||
.console-container::-webkit-scrollbar-thumb {
|
||||
background-color: #8888884d; /* set thumb color */
|
||||
border-radius: 5px; /* round corners */
|
||||
}
|
||||
|
||||
.console-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #88888883; /* set thumb color */
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import './css/home.css'
|
||||
|
||||
import Sessions from './components/sessions'
|
||||
import StatCards from './components/StatsCards'
|
||||
import WatchStatistics from './components/WatchStatistics'
|
||||
import LibraryOverView from './components/libraryOverview'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function Home() {
|
||||
<div>
|
||||
|
||||
<Sessions />
|
||||
<StatCards/>
|
||||
<WatchStatistics/>
|
||||
<LibraryOverView/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import "./css/libraries.css";
|
||||
import "./css/usersactivity.css";
|
||||
import "./css/users.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
|
||||
import SettingsConfig from "./components/settings/settingsConfig";
|
||||
import LibrarySync from "./components/settings/librarySync";
|
||||
import WebSocketComponent from "./components/settings/WebSocketComponent ";
|
||||
|
||||
import TerminalComponent from "./components/settings/TerminalComponent";
|
||||
|
||||
import "./css/settings.css";
|
||||
|
||||
@@ -13,7 +14,7 @@ export default function Settings() {
|
||||
<div>
|
||||
<SettingsConfig/>
|
||||
<LibrarySync/>
|
||||
<WebSocketComponent/>
|
||||
<TerminalComponent/>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,59 @@ function Setup() {
|
||||
function handleFormChange(event) {
|
||||
setFormValues({ ...formValues, [event.target.name]: event.target.value });
|
||||
}
|
||||
async function beginSync() {
|
||||
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
await axios
|
||||
.get("/sync/writeLibraries")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
await axios
|
||||
.get("/sync/writeLibraryItems")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
|
||||
await axios
|
||||
.get("/sync/writeSeasonsAndEpisodes")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
|
||||
await axios
|
||||
.get("/sync/writeUsers")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
setProcessing(false);
|
||||
// return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
async function validateSettings(_url, _apikey) {
|
||||
// Send a GET request to /system/configuration to test copnnection
|
||||
@@ -73,12 +126,13 @@ function Setup() {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
setsubmitButtonText("Settings Saved");
|
||||
setProcessing(false);
|
||||
setTimeout(() => {
|
||||
window.location.href = "/";
|
||||
}, 1000);
|
||||
await beginSync();
|
||||
|
||||
return;
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import './css/libraries.css';
|
||||
import Loading from './components/loading';
|
||||
|
||||
// import PlaybackActivity from './components/playbackactivity';
|
||||
|
||||
import Activity from './activity';
|
||||
// import StatCards from './components/StatsCards';
|
||||
|
||||
import LibraryOverView from './components/libraryOverview';
|
||||
@@ -12,7 +12,7 @@ import LibraryOverView from './components/libraryOverview';
|
||||
import API from '../classes/jellyfin-api';
|
||||
|
||||
|
||||
function UserData() {
|
||||
function Testing() {
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,4 +49,4 @@ function UserData() {
|
||||
);
|
||||
}
|
||||
|
||||
export default UserData;
|
||||
export default Testing;
|
||||
14
src/pages/user-info.js
Normal file
14
src/pages/user-info.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
function UserInfo() {
|
||||
const { UserId } = useParams();
|
||||
|
||||
// Fetch data for the user with the specified userId
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{UserId}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default UserInfo;
|
||||
@@ -1,138 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
|
||||
import "./css/usersactivity.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
function UserActivity() {
|
||||
const [data, setData] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/user_usage_stats/user_activity?days=9999`;
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": apiKey,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
return (
|
||||
<div className="Users">
|
||||
<h1>Users</h1>
|
||||
<table className="user-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th onClick={() => handleSort("user_name")}>User</th>
|
||||
<th onClick={() => handleSort("item_name")}>Last Watched</th>
|
||||
<th onClick={() => handleSort("client_name")}>Last Client</th>
|
||||
<th onClick={() => handleSort("total_count")}>Total Plays</th>
|
||||
<th onClick={() => handleSort("total_play_time")}>
|
||||
Total Watch Time
|
||||
</th>
|
||||
<th onClick={() => handleSort("last_seen")}>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedData.map((item) => (
|
||||
<tr key={item.user_id}>
|
||||
<td>
|
||||
{item.has_image ? (
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Users/" +
|
||||
item.user_id +
|
||||
"/Images/Primary?quality=10"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon color="#fff" size={30} />
|
||||
)}
|
||||
</td>
|
||||
<td>{item.user_name}</td>
|
||||
<td>{item.item_name}</td>
|
||||
<td>{item.client_name}</td>
|
||||
<td>{item.total_count}</td>
|
||||
<td>{item.total_play_time}</td>
|
||||
<td>{item.last_seen} ago</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserActivity;
|
||||
190
src/pages/users.js
Normal file
190
src/pages/users.js
Normal file
@@ -0,0 +1,190 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
import { Link } from 'react-router-dom';
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
|
||||
import "./css/users.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
function Users() {
|
||||
const [data, setData] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [itemCount,setItemCount] = useState(10);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function formatTime(time) {
|
||||
const units = {
|
||||
days: ['Day', 'Days'],
|
||||
hours: ['Hour', 'Hours'],
|
||||
minutes: ['Minute', 'Minutes'],
|
||||
seconds: ['Second', 'Seconds']
|
||||
};
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getAllUserActivity`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
|
||||
const indexOfLastUser = currentPage * itemCount;
|
||||
const indexOfFirstUser = indexOfLastUser - itemCount;
|
||||
const currentUsers = sortedData.slice(indexOfFirstUser, indexOfLastUser);
|
||||
|
||||
const pageNumbers = [];
|
||||
for (let i = 1; i <= Math.ceil(sortedData.length / itemCount); i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="Users">
|
||||
<div className="Heading">
|
||||
<h1>All Users</h1>
|
||||
<div className="pagination-range">
|
||||
<div className="header">Items</div>
|
||||
<select value={itemCount} onChange={(event) => {setItemCount(event.target.value); setCurrentPage(1);}}>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="user-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th onClick={() => handleSort("UserName")}>User</th>
|
||||
<th onClick={() => handleSort("LastWatched")}>Last Watched</th>
|
||||
<th onClick={() => handleSort("LastClient")}>Last Client</th>
|
||||
<th onClick={() => handleSort("TotalPlays")}>Total Plays</th>
|
||||
<th onClick={() => handleSort("TotalWatchTime")}>Total Watch Time</th>
|
||||
<th onClick={() => handleSort("LastSeen")}>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentUsers.map((item) => (
|
||||
<tr key={item.UserId}>
|
||||
<td>
|
||||
{item.PrimaryImageTag ? (
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Users/" +
|
||||
item.UserId +
|
||||
"/Images/Primary?quality=10"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon color="#fff" size={30} />
|
||||
)}
|
||||
</td>
|
||||
<td> <Link to={`/user-info/${item.UserId}`}>{item.UserName}</Link></td>
|
||||
<td>{item.LastWatched || 'never'}</td>
|
||||
<td>{item.LastClient || 'n/a'}</td>
|
||||
<td>{item.TotalPlays}</td>
|
||||
<td>{item.TotalWatchTime || 0}</td>
|
||||
<td>{item.LastSeen ? formatTime(item.LastSeen) : 'never'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<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 Users;
|
||||
Reference in New Issue
Block a user