From 582a39918e3547e0a7225f590e7f61a45588fb24 Mon Sep 17 00:00:00 2001 From: Thegan Govender Date: Sun, 19 Mar 2023 22:01:40 +0200 Subject: [PATCH] full change to statistics 1) Created components for statistic reporting. 2) Database changes and PROC/Function creations. Still need to make MOST VIEWED LIBRARIES/CLIENTS/ MOST ACTIVE USERS dynamically load with date range (Function Creation on DB side) --- SQL Scripts/1. CREATE DATABASE jfstat.sql | 13 + .../CREATE TABLE jf_activity_watchdog.sql | 27 ++ .../CREATE TABLE jf_library_episodes.sql | 32 ++ .../CREATE TABLE app_config.sql | 18 + .../CREATE TABLE jf_libraries.sql | 20 + .../CREATE TABLE jf_library_items.sql | 38 ++ .../CREATE TABLE jf_library_seasons.sql | 24 + .../CREATE TABLE jf_playback_activity.sql | 27 ++ .../CREATE FUNCTION fs_most_played_items.sql | 45 ++ .../CREATE FUNCTION fs_most_popular_items.sql | 52 +++ ...EATE FUNCTION fs_most_viewed_libraries.sql | 48 ++ SQL Scripts/2. CREATE USER jfstat.sql | 11 + .../CREATE VIEW jf_library_count_view.sql | 22 + .../CREATE VIEW js_most_active_user.sql | 16 + .../CREATE VIEW js_most_used_clients.sql | 15 + backend/api.js | 17 +- backend/db.js | 80 +++- backend/models/jf_activity_watchdog.js | 41 ++ backend/models/jf_libraries.js | 26 ++ backend/models/jf_library_episodes.js | 52 +++ backend/models/jf_library_items.js | 49 ++ backend/models/jf_library_seasons.js | 36 ++ backend/models/jf_playback_activity.js | 42 ++ backend/server.js | 2 + backend/stats.js | 95 ++++ backend/sync.js | 421 +++++++----------- backend/watchdog/ActivityMonitor.js | 170 +++++++ package-lock.json | 1 + package.json | 1 + src/App.css | 67 --- src/pages/activity.js | 13 +- src/pages/components/ComponentLoading.js | 12 + src/pages/components/StatsCards.js | 61 +++ src/pages/components/libraryOverview.js | 119 +++-- src/pages/components/playbackactivity.js | 125 ++++++ src/pages/components/session-card.js | 2 +- src/pages/components/sessions.js | 38 +- .../settings/WebSocketComponent .js | 4 +- src/pages/components/settings/librarySync.js | 36 +- .../components/settings/settingsConfig.js | 2 +- .../components/statCards/most_active_users.js | 127 ++++++ .../components/statCards/most_used_client.js | 107 +++++ src/pages/components/statCards/mp_movies.js | 130 ++++++ src/pages/components/statCards/mp_series.js | 131 ++++++ .../components/statCards/mv_libraries.js | 121 +++++ src/pages/components/statCards/mv_movies.js | 130 ++++++ src/pages/components/statCards/mv_series.js | 130 ++++++ src/pages/css/activity.css | 57 +++ src/pages/css/libraryOverview.css | 161 ++++--- src/pages/css/loading.css | 11 +- src/pages/css/sessions.css | 17 +- src/pages/css/settings.css | 1 + src/pages/css/statCard.css | 170 +++++++ src/pages/css/usersactivity.css | 21 +- src/pages/home.js | 8 +- src/pages/libraries.js | 87 +++- src/pages/setup.js | 2 +- src/pages/userdata.js | 15 +- src/setupProxy.js | 26 ++ 59 files changed, 2882 insertions(+), 490 deletions(-) create mode 100644 SQL Scripts/1. CREATE DATABASE jfstat.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_activity_watchdog.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_episodes.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_items.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_seasons.sql create mode 100644 SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_playback_activity.sql create mode 100644 SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_played_items.sql create mode 100644 SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_popular_items.sql create mode 100644 SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_viewed_libraries.sql create mode 100644 SQL Scripts/2. CREATE USER jfstat.sql create mode 100644 SQL Scripts/3. CREATE VIEWS/CREATE VIEW jf_library_count_view.sql create mode 100644 SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_active_user.sql create mode 100644 SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_used_clients.sql create mode 100644 backend/models/jf_activity_watchdog.js create mode 100644 backend/models/jf_libraries.js create mode 100644 backend/models/jf_library_episodes.js create mode 100644 backend/models/jf_library_items.js create mode 100644 backend/models/jf_library_seasons.js create mode 100644 backend/models/jf_playback_activity.js create mode 100644 backend/watchdog/ActivityMonitor.js create mode 100644 src/pages/components/ComponentLoading.js create mode 100644 src/pages/components/StatsCards.js create mode 100644 src/pages/components/playbackactivity.js create mode 100644 src/pages/components/statCards/most_active_users.js create mode 100644 src/pages/components/statCards/most_used_client.js create mode 100644 src/pages/components/statCards/mp_movies.js create mode 100644 src/pages/components/statCards/mp_series.js create mode 100644 src/pages/components/statCards/mv_libraries.js create mode 100644 src/pages/components/statCards/mv_movies.js create mode 100644 src/pages/components/statCards/mv_series.js create mode 100644 src/pages/css/activity.css create mode 100644 src/pages/css/statCard.css create mode 100644 src/setupProxy.js diff --git a/SQL Scripts/1. CREATE DATABASE jfstat.sql b/SQL Scripts/1. CREATE DATABASE jfstat.sql new file mode 100644 index 0000000..4cda4db --- /dev/null +++ b/SQL Scripts/1. CREATE DATABASE jfstat.sql @@ -0,0 +1,13 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_activity_watchdog.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_activity_watchdog.sql new file mode 100644 index 0000000..ac5ebc3 --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_activity_watchdog.sql @@ -0,0 +1,27 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_episodes.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_episodes.sql new file mode 100644 index 0000000..f88907b --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_episodes.sql @@ -0,0 +1,32 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql new file mode 100644 index 0000000..1e65126 --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql @@ -0,0 +1,18 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql new file mode 100644 index 0000000..bca0452 --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql @@ -0,0 +1,20 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_items.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_items.sql new file mode 100644 index 0000000..9c37393 --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_items.sql @@ -0,0 +1,38 @@ +-- 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'; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_seasons.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_seasons.sql new file mode 100644 index 0000000..63d7eaa --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_seasons.sql @@ -0,0 +1,24 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_playback_activity.sql b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_playback_activity.sql new file mode 100644 index 0000000..5826794 --- /dev/null +++ b/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_playback_activity.sql @@ -0,0 +1,27 @@ +-- 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; \ No newline at end of file diff --git a/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_played_items.sql b/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_played_items.sql new file mode 100644 index 0000000..bb4a552 --- /dev/null +++ b/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_played_items.sql @@ -0,0 +1,45 @@ +-- 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; diff --git a/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_popular_items.sql b/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_popular_items.sql new file mode 100644 index 0000000..54cf83a --- /dev/null +++ b/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_popular_items.sql @@ -0,0 +1,52 @@ +-- 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; diff --git a/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_viewed_libraries.sql b/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_viewed_libraries.sql new file mode 100644 index 0000000..459f42e --- /dev/null +++ b/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_viewed_libraries.sql @@ -0,0 +1,48 @@ +-- 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; diff --git a/SQL Scripts/2. CREATE USER jfstat.sql b/SQL Scripts/2. CREATE USER jfstat.sql new file mode 100644 index 0000000..cdabf5a --- /dev/null +++ b/SQL Scripts/2. CREATE USER jfstat.sql @@ -0,0 +1,11 @@ +-- 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='; \ No newline at end of file diff --git a/SQL Scripts/3. CREATE VIEWS/CREATE VIEW jf_library_count_view.sql b/SQL Scripts/3. CREATE VIEWS/CREATE VIEW jf_library_count_view.sql new file mode 100644 index 0000000..78f2357 --- /dev/null +++ b/SQL Scripts/3. CREATE VIEWS/CREATE VIEW jf_library_count_view.sql @@ -0,0 +1,22 @@ +-- 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; + diff --git a/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_active_user.sql b/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_active_user.sql new file mode 100644 index 0000000..4d8239e --- /dev/null +++ b/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_active_user.sql @@ -0,0 +1,16 @@ +-- 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; + diff --git a/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_used_clients.sql b/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_used_clients.sql new file mode 100644 index 0000000..67ed567 --- /dev/null +++ b/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_used_clients.sql @@ -0,0 +1,15 @@ +-- 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; + diff --git a/backend/api.js b/backend/api.js index 2301abd..2f38550 100644 --- a/backend/api.js +++ b/backend/api.js @@ -12,7 +12,7 @@ router.get("/test", async (req, res) => { router.get("/getconfig", async (req, res) => { const { rows } = await db.query('SELECT * FROM app_config where "ID"=1'); - console.log(`ENDPOINT CALLED: /getconfig: ` + rows); + // console.log(`ENDPOINT CALLED: /getconfig: ` + rows); // console.log(`ENDPOINT CALLED: /setconfig: `+rows.length); res.send(rows); }); @@ -43,7 +43,20 @@ router.get("/getAllFromJellyfin", async (req, res) => { res.send(results); - console.log(`ENDPOINT CALLED: /getAllFromJellyfin: `); + // console.log(`ENDPOINT CALLED: /getAllFromJellyfin: `); +}); + + +router.post("/getLibraryItems", async (req, res) => { + const Id = req.headers['id']; + + const { rows } = await db.query( + `SELECT * FROM jf_library_items where "ParentId"='${Id}'` + ); + console.log({ Id: Id }); + res.send(rows); + + console.log(`ENDPOINT CALLED: /getLibraryItems: `); }); module.exports = router; diff --git a/backend/db.js b/backend/db.js index 69511b3..f5ef9ca 100644 --- a/backend/db.js +++ b/backend/db.js @@ -1,14 +1,82 @@ // db.js const { Pool } = require('pg'); +const pgp = require("pg-promise")(); const pool = new Pool({ - user: 'jfstat', - host: '10.0.0.99', - database: 'jfstat', - password: '123456', - port: 32778, // or your PostgreSQL port number - }); + user: 'jfstat', + host: '10.0.0.99', + database: 'jfstat', + password: '123456', + port: 32778, // or your PostgreSQL port number +}); + +async function deleteBulk(table_name, data) { + const client = await pool.connect(); + let result='SUCCESS'; + let message=''; + try { + await client.query('BEGIN'); + + // const AllIds = data.map((row) => row.Id); + + if (data.length !== 0) { + + const deleteQuery = { + text: `DELETE FROM ${table_name} WHERE "Id" IN (${pgp.as.csv( + data + )})` + }; + // console.log(deleteQuery); + await client.query(deleteQuery); + } + // else { + // await client.query(`DELETE FROM ${table_name}`); + // console.log('Delete All'); + // } + + await client.query('COMMIT'); + message=(data.length + " Rows removed."); + + } catch (error) { + await client.query('ROLLBACK'); + message=('Error: '+ error); + result='ERROR'; + } finally { + client.release(); + } + return ({Result:result,message:message}); +} + +async function insertBulk(table_name, data,columns) { + const client = await pool.connect(); + let result='SUCCESS'; + let message=''; + try { + await client.query("BEGIN"); + + const query = pgp.helpers.insert( + data, + columns, + table_name + ); + await client.query(query); + + await client.query("COMMIT"); + + message=(data.length + " Rows Inserted."); + + } catch (error) { + await client.query('ROLLBACK'); + message=('Error: '+ error); + result='ERROR'; + } finally { + client.release(); + } + return ({Result:result,message:message}); +} module.exports = { query: (text, params) => pool.query(text, params), + deleteBulk: deleteBulk, + insertBulk: insertBulk, }; diff --git a/backend/models/jf_activity_watchdog.js b/backend/models/jf_activity_watchdog.js new file mode 100644 index 0000000..9eba789 --- /dev/null +++ b/backend/models/jf_activity_watchdog.js @@ -0,0 +1,41 @@ +const jf_activity_watchdog_columns = [ + "Id", + "IsPaused", + "UserId", + "UserName", + "Client", + "DeviceName", + "DeviceId", + "ApplicationVersion", + "NowPlayingItemId", + "NowPlayingItemName", + "EpisodeId", + "SeasonId", + "SeriesName", + "PlaybackDuration", + "ActivityDateInserted", + ]; + + + const jf_activity_watchdog_mapping = (item) => ({ + Id: item.Id , + IsPaused: item.PlayState.IsPaused !== undefined ? item.PlayState.IsPaused : item.IsPaused, + UserId: item.UserId, + UserName: item.UserName, + Client: item.Client, + DeviceName: item.DeviceName, + DeviceId: item.DeviceId, + ApplicationVersion: item.ApplicationVersion, + NowPlayingItemId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.SeriesId : item.NowPlayingItem.Id, + NowPlayingItemName: item.NowPlayingItem.Name, + EpisodeId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.Id: null, + SeasonId: item.NowPlayingItem.SeasonId || null, + SeriesName: item.NowPlayingItem.SeriesName || null, + PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0, + ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(), + }); + + module.exports = { + jf_activity_watchdog_columns, + jf_activity_watchdog_mapping + }; \ No newline at end of file diff --git a/backend/models/jf_libraries.js b/backend/models/jf_libraries.js new file mode 100644 index 0000000..bed869e --- /dev/null +++ b/backend/models/jf_libraries.js @@ -0,0 +1,26 @@ + ////////////////////////// pn delete move to playback + const jf_libraries_columns = [ + "Id", + "Name", + "ServerId", + "IsFolder", + "Type", + "CollectionType", + "ImageTagsPrimary", + ]; + + const jf_libraries_mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + ServerId: item.ServerId, + IsFolder: item.IsFolder, + Type: item.Type, + CollectionType: item.CollectionType, + ImageTagsPrimary: + item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, + }); + + module.exports = { + jf_libraries_columns, + jf_libraries_mapping, + }; \ No newline at end of file diff --git a/backend/models/jf_library_episodes.js b/backend/models/jf_library_episodes.js new file mode 100644 index 0000000..722148d --- /dev/null +++ b/backend/models/jf_library_episodes.js @@ -0,0 +1,52 @@ + ////////////////////////// pn delete move to playback + const jf_library_episodes_columns = [ + "Id", + "EpisodeId", + "Name", + "ServerId", + "PremiereDate", + "OfficialRating", + "CommunityRating", + "RunTimeTicks", + "ProductionYear", + "IndexNumber", + "ParentIndexNumber", + "Type", + "ParentLogoItemId", + "ParentBackdropItemId", + "ParentBackdropImageTags", + "SeriesId", + "SeasonId", + "SeasonName", + "SeriesName", + ]; + + const jf_library_episodes_mapping = (item) => ({ + Id: item.Id + item.ParentId, + EpisodeId: item.Id, + Name: item.Name, + ServerId: item.ServerId, + PremiereDate: item.PremiereDate, + OfficialRating: item.OfficialRating, + CommunityRating: item.CommunityRating, + RunTimeTicks: item.RunTimeTicks, + ProductionYear: item.ProductionYear, + IndexNumber: item.IndexNumber, + ParentIndexNumber: item.ParentIndexNumber, + Type: item.Type, + ParentLogoItemId: item.ParentLogoItemId, + ParentBackdropItemId: item.ParentBackdropItemId, + ParentBackdropImageTags: + item.ParentBackdropImageTags !== undefined + ? item.ParentBackdropImageTags[0] + : null, + SeriesId: item.SeriesId, + SeasonId: item.ParentId, + SeasonName: item.SeasonName, + SeriesName: item.SeriesName, + }); + + module.exports = { + jf_library_episodes_columns, + jf_library_episodes_mapping, + }; \ No newline at end of file diff --git a/backend/models/jf_library_items.js b/backend/models/jf_library_items.js new file mode 100644 index 0000000..dd9d944 --- /dev/null +++ b/backend/models/jf_library_items.js @@ -0,0 +1,49 @@ + + const jf_library_items_columns = [ + "Id", + "Name", + "ServerId", + "PremiereDate", + "EndDate", + "CommunityRating", + "RunTimeTicks", + "ProductionYear", + "IsFolder", + "Type", + "Status", + "ImageTagsPrimary", + "ImageTagsBanner", + "ImageTagsLogo", + "ImageTagsThumb", + "BackdropImageTags", + "ParentId", + ]; + + const jf_library_items_mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + ServerId: item.ServerId, + PremiereDate: item.PremiereDate, + EndDate: item.EndDate, + CommunityRating: item.CommunityRating, + RunTimeTicks: item.RunTimeTicks, + ProductionYear: item.ProductionYear, + IsFolder: item.IsFolder, + Type: item.Type, + Status: item.Status, + ImageTagsPrimary: + item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, + ImageTagsBanner: + item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null, + ImageTagsLogo: + item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null, + ImageTagsThumb: + item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null, + BackdropImageTags: item.BackdropImageTags[0], + ParentId: item.ParentId, + }); + + module.exports = { + jf_library_items_columns, + jf_library_items_mapping, + }; \ No newline at end of file diff --git a/backend/models/jf_library_seasons.js b/backend/models/jf_library_seasons.js new file mode 100644 index 0000000..c94d851 --- /dev/null +++ b/backend/models/jf_library_seasons.js @@ -0,0 +1,36 @@ + ////////////////////////// pn delete move to playback + const jf_library_seasons_columns = [ + "Id", + "Name", + "ServerId", + "IndexNumber", + "Type", + "ParentLogoItemId", + "ParentBackdropItemId", + "ParentBackdropImageTags", + "SeriesName", + "SeriesId", + "SeriesPrimaryImageTag", + ]; + + const jf_library_seasons_mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + ServerId: item.ServerId, + IndexNumber: item.IndexNumber, + Type: item.Type, + ParentLogoItemId: item.ParentLogoItemId, + ParentBackdropItemId: item.ParentBackdropItemId, + ParentBackdropImageTags: + item.ParentBackdropImageTags !== undefined + ? item.ParentBackdropImageTags[0] + : null, + SeriesName: item.SeriesName, + SeriesId: item.ParentId, + SeriesPrimaryImageTag: item.SeriesPrimaryImageTag, + }); + + module.exports = { + jf_library_seasons_columns, + jf_library_seasons_mapping, + }; \ No newline at end of file diff --git a/backend/models/jf_playback_activity.js b/backend/models/jf_playback_activity.js new file mode 100644 index 0000000..a38ed9f --- /dev/null +++ b/backend/models/jf_playback_activity.js @@ -0,0 +1,42 @@ + ////////////////////////// pn delete move to playback + const columnsPlayback = [ + "Id", + "IsPaused", + "UserId", + "UserName", + "Client", + "DeviceName", + "DeviceId", + "ApplicationVersion", + "NowPlayingItemId", + "NowPlayingItemName", + "EpisodeId", + "SeasonId", + "SeriesName", + "PlaybackDuration", + "ActivityDateInserted", + ]; + + + const mappingPlayback = (item) => ({ + Id: item.Id , + IsPaused: item.PlayState.IsPaused !== undefined ? item.PlayState.IsPaused : item.IsPaused, + UserId: item.UserId, + UserName: item.UserName, + Client: item.Client, + DeviceName: item.DeviceName, + DeviceId: item.DeviceId, + ApplicationVersion: item.ApplicationVersion, + NowPlayingItemId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.SeriesId : item.NowPlayingItem.Id, + NowPlayingItemName: item.NowPlayingItem.Name, + EpisodeId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.Id: null, + SeasonId: item.NowPlayingItem.SeasonId || null, + SeriesName: item.NowPlayingItem.SeriesName || null, + PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0, + ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(), + }); + + module.exports = { + columnsPlayback, + mappingPlayback, + }; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index c86ace0..d373ca0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,8 @@ const cors = require('cors'); const apiRouter = require('./api'); const syncRouter = require('./sync'); const statsRouter = require('./stats'); +const ActivityMonitor=require('./watchdog/ActivityMonitor'); +ActivityMonitor.ActivityMonitor(1000); const app = express(); const PORT = process.env.PORT || 3003; diff --git a/backend/stats.js b/backend/stats.js index ccfbcb6..62922c4 100644 --- a/backend/stats.js +++ b/backend/stats.js @@ -15,4 +15,99 @@ router.get("/getLibraryOverview", async (req, res) => { console.log(`ENDPOINT CALLED: /getLibraryOverview`); }); + +router.post("/getMostViewedSeries", 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},'Series') limit 5` + ); + res.send(rows); + +}); + + +router.post("/getMostViewedMovies", 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},'Movie') limit 5` + ); + res.send(rows); + +}); + + + +router.post("/getMostViewedLibraries", async (req, res) => { + const {days} = req.body; + let _days=days; + if(days===undefined) + { + _days=30; + } + const { rows } = await db.query( + `select * from fs_most_viewed_libraries(${_days})` + ); + res.send(rows); + +}); + +router.get("/getMostUsedClient", async (req, res) => { + const { rows } = await db.query('SELECT * FROM js_most_used_clients limit 5'); + res.send(rows); +}); + +router.get("/getMostActiveUsers", async (req, res) => { + const { rows } = await db.query('SELECT * FROM js_most_active_user limit 5'); + res.send(rows); +}); + + +router.post("/getMostPopularMovies", 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},'Movie') limit 5` + ); + res.send(rows); + +}); + + + +router.post("/getMostPopularSeries", async (req, res) => { + const {days} = req.body; + let _days=days; + if(days===undefined) + { + _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` + ); + res.send(rows); + +}); + + +router.get("/getPlaybackActivity", async (req, res) => { + const { rows } = await db.query('SELECT * FROM jf_playback_activity'); + res.send(rows); + // console.log(`ENDPOINT CALLED: /getPlaybackActivity`); +}); + module.exports = router; diff --git a/backend/sync.js b/backend/sync.js index b74e8eb..776e215 100644 --- a/backend/sync.js +++ b/backend/sync.js @@ -8,7 +8,22 @@ 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"); /////////////////////////////////////////Functions class sync { @@ -95,49 +110,32 @@ 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!" }); + sendMessageToClients({ Message: "Error: Config details not found!" }); return; } const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); const data = await _sync.getItem(); //getting all root folders aka libraries - const columns = [ - "Id", - "Name", - "ServerId", - "IsFolder", - "Type", - "CollectionType", - "ImageTagsPrimary", - ]; // specify the columns to insert into + // specify the columns to insert into const existingIds = await db .query('SELECT "Id" FROM jf_libraries') .then((res) => res.rows.map((row) => row.Id)); // get existing library Ids from the db //data mapping - const mapping = (item) => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - IsFolder: item.IsFolder, - Type: item.Type, - CollectionType: item.CollectionType, - ImageTagsPrimary: - item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, - }); + 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(mapping); + dataToInsert = await data.map(jf_libraries_mapping); } else { // otherwise, filter only new data to insert dataToInsert = await data .filter((row) => !existingIds.includes(row.Id)) - .map(mapping); + .map(jf_libraries_mapping); } //Bulkinsert new data not on db @@ -147,7 +145,11 @@ router.get("/writeLibraries", async (req, res) => { try { await db.query("BEGIN"); - const query = pgp.helpers.insert(dataToInsert, columns, "jf_libraries"); + const query = pgp.helpers.insert( + dataToInsert, + jf_libraries_columns, + "jf_libraries" + ); await db.query(query); await db.query("COMMIT"); @@ -162,12 +164,14 @@ router.get("/writeLibraries", async (req, res) => { Type: "Error", Message: "Error performing bulk insert:" + error, }); - sendMessageToClients({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"}); + sendMessageToClients({ Message: "No new data to bulk insert" }); } //Bulk delete from db thats no longer on api if (existingIds.length > data.length) { @@ -193,7 +197,9 @@ router.get("/writeLibraries", async (req, res) => { Type: "Success", Message: existingIds.length - data.length + " Rows Removed.", }); - sendMessageToClients(existingIds.length - data.length + " Rows Removed."); + sendMessageToClients( + existingIds.length - data.length + " Rows Removed." + ); } catch (error) { await db.query("ROLLBACK"); @@ -201,16 +207,17 @@ router.get("/writeLibraries", async (req, res) => { Type: "Error", Message: "Error performing bulk removal:" + error, }); - sendMessageToClients({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"}); + sendMessageToClients({ Message: "No new data to bulk delete" }); } //Sent logs - res.send(message); console.log(`ENDPOINT CALLED: /writeLibraries: `); @@ -228,11 +235,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(); 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]; @@ -250,62 +264,19 @@ router.get("/writeLibraryItems", async (req, res) => { .query('SELECT "Id" FROM jf_library_items') .then((res) => res.rows.map((row) => row.Id)); - //Mappings to store data in DB - const columns = [ - "Id", - "Name", - "ServerId", - "PremiereDate", - "EndDate", - "CommunityRating", - "RunTimeTicks", - "ProductionYear", - "IsFolder", - "Type", - "Status", - "ImageTagsPrimary", - "ImageTagsBanner", - "ImageTagsLogo", - "ImageTagsThumb", - "BackdropImageTags", - "ParentId", - ]; // specify the columns to insert into - //data mapping - const mapping = (item) => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - PremiereDate: item.PremiereDate, - EndDate: item.EndDate, - CommunityRating: item.CommunityRating, - RunTimeTicks: item.RunTimeTicks, - ProductionYear: item.ProductionYear, - IsFolder: item.IsFolder, - Type: item.Type, - Status: item.Status, - ImageTagsPrimary: - item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, - ImageTagsBanner: - item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null, - ImageTagsLogo: - item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null, - ImageTagsThumb: - item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null, - BackdropImageTags: item.BackdropImageTags[0], - ParentId: item.ParentId, - }); + 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(mapping); + 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(mapping); + .map(jf_library_items_mapping); } //Bulkinsert new data not on db @@ -317,7 +288,7 @@ router.get("/writeLibraryItems", async (req, res) => { const query = pgp.helpers.insert( dataToInsert, - columns, + jf_library_items_columns, "jf_library_items" ); await db.query(query); @@ -327,12 +298,17 @@ router.get("/writeLibraryItems", async (req, res) => { 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 { @@ -362,6 +338,7 @@ router.get("/writeLibraryItems", async (req, res) => { Type: "Success", Message: existingIds.length - data.length + " Rows Removed.", }); + deleteCounter += existingIds.length - data.length; } catch (error) { await db.query("ROLLBACK"); @@ -369,13 +346,28 @@ router.get("/writeLibraryItems", async (req, res) => { 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.", + }); + sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" }); + res.send(message); console.log(`ENDPOINT CALLED: /writeLibraryItems: `); @@ -383,7 +375,11 @@ router.get("/writeLibraryItems", async (req, res) => { //////////////////////////////////////////////////////writeSeasonsAndEpisodes router.get("/writeSeasonsAndEpisodes", async (req, res) => { - sendMessageToClients({color:'yellow',Message:"Beginning Seasons and Episode sync"}); + 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' @@ -398,6 +394,10 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { `SELECT * FROM public.jf_library_items where "Type"='Series'` ); + let insertSeasonsCount = 0; + let insertEpisodeCount = 0; + let deleteSeasonsCount = 0; + let deleteEpisodeCount = 0; //loop for each show for (const show of shows) { const data = await _sync.getSeasonsAndEpisodes(show.Id); @@ -405,17 +405,12 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { // //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)); + 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 = []; if (existingIdsSeasons.length > 0) { existingIdsEpisodes = await db - .query( - `SELECT * FROM public.jf_library_episodes WHERE "SeasonId" IN (${existingIdsSeasons + .query(`SELECT * FROM public.jf_library_episodes WHERE "SeasonId" IN (${existingIdsSeasons .filter((seasons) => seasons !== "") .map((seasons) => pgp.as.value(seasons)) .map((value) => "'" + value + "'") @@ -424,85 +419,6 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { .then((res) => res.rows.map((row) => row.Id)); } - //Mappings to store data in DB - const columnSeasons = [ - "Id", - "Name", - "ServerId", - "IndexNumber", - "Type", - "ParentLogoItemId", - "ParentBackdropItemId", - "ParentBackdropImageTags", - "SeriesName", - "SeriesId", - "SeriesPrimaryImageTag", - ]; // specify the columns to insert into - const columnEpisodes = [ - "Id", - "EpisodeId", - "Name", - "ServerId", - "PremiereDate", - "OfficialRating", - "CommunityRating", - "RunTimeTicks", - "ProductionYear", - "IndexNumber", - "ParentIndexNumber", - "Type", - "ParentLogoItemId", - "ParentBackdropItemId", - "ParentBackdropImageTags", - "SeriesId", - "SeasonId", - "SeasonName", - "SeriesName", - ]; // specify the columns to insert into - - //data mapping - const seasonsmapping = (item) => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - IndexNumber: item.IndexNumber, - Type: item.Type, - ParentLogoItemId: item.ParentLogoItemId, - ParentBackdropItemId: item.ParentBackdropItemId, - ParentBackdropImageTags: - item.ParentBackdropImageTags !== undefined - ? item.ParentBackdropImageTags[0] - : null, - SeriesName: item.SeriesName, - SeriesId: item.ParentId, - SeriesPrimaryImageTag: item.SeriesPrimaryImageTag, - }); - - const episodemapping = (item) => ({ - Id: item.Id + item.ParentId, - EpisodeId: item.Id, - Name: item.Name, - ServerId: item.ServerId, - PremiereDate: item.PremiereDate, - OfficialRating: item.OfficialRating, - CommunityRating: item.CommunityRating, - RunTimeTicks: item.RunTimeTicks, - ProductionYear: item.ProductionYear, - IndexNumber: item.IndexNumber, - ParentIndexNumber: item.ParentIndexNumber, - Type: item.Type, - ParentLogoItemId: item.ParentLogoItemId, - ParentBackdropItemId: item.ParentBackdropItemId, - ParentBackdropImageTags: - item.ParentBackdropImageTags !== undefined - ? item.ParentBackdropImageTags[0] - : null, - SeriesId: item.SeriesId, - SeasonId: item.ParentId, - SeasonName: item.SeasonName, - SeriesName: item.SeriesName, - }); - // let seasonsToInsert = []; @@ -511,58 +427,46 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { if (existingIdsSeasons.length === 0) { // if there are no existing Ids in the table, map all items in the data array to the expected format - seasonsToInsert = await data.allSeasons.map(seasonsmapping); + seasonsToInsert = await data.allSeasons.map(jf_library_seasons_mapping); } else { // otherwise, filter only new data to insert seasonsToInsert = await data.allSeasons .filter((row) => !existingIdsSeasons.includes(row.Id)) - .map(seasonsmapping); + .map(jf_library_seasons_mapping); } if (existingIdsEpisodes.length === 0) { // if there are no existing Ids in the table, map all items in the data array to the expected format - episodesToInsert = await data.allEpisodes.map(episodemapping); + episodesToInsert = await data.allEpisodes.map(jf_library_episodes_mapping); } else { // otherwise, filter only new data to insert - episodesToInsert = await data.allEpisodes - .filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId)) - .map(episodemapping); + episodesToInsert = await data.allEpisodes.filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId)).map(jf_library_episodes_mapping); } ///insert delete seasons //Bulkinsert new data not on db if (seasonsToInsert.length !== 0) { - //insert new - await (async () => { - try { - await db.query("BEGIN"); - - const query = pgp.helpers.insert( - seasonsToInsert, - columnSeasons, - "jf_library_seasons" - ); - await db.query(query); - - await db.query("COMMIT"); - message.push({ - Type: "Success", - Message: seasonsToInsert.length + " Rows Inserted for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_seasons", - }); - sendMessageToClients({color:'cornflowerblue',Message:seasonsToInsert.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_seasons", - }); - sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error}); - } - })(); + 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", @@ -570,51 +474,38 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_seasons", }); - sendMessageToClients({Message:"No new data to bulk insert for " + show.Name}); } + + const toDeleteIds = existingIdsSeasons.filter((id) =>!data.allSeasons.some((row) => row.Id === id )); //Bulk delete from db thats no longer on api - if (existingIdsSeasons.length > data.allSeasons.length) { - await (async () => { - try { - await db.query("BEGIN"); + if (toDeleteIds.length > 0) { - const AllIds = data.allSeasons.map((row) => row.Id); + + let table="jf_library_seasons"; + let result = await db.deleteBulk(table,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, + }); - const deleteQuery = { - text: `DELETE FROM jf_library_seasons 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: - existingIdsSeasons.length - - data.allSeasons.length + - " Rows Removed for " + - show.Name, - ItemId: show.Id, - TableName: "jf_library_seasons", - }); - sendMessageToClients({color:'orange',Message:existingIdsSeasons.length -data.allSeasons.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_seasons", - }); - sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error}); - } - })(); + + } + } else { message.push({ Type: "Success", @@ -622,7 +513,7 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_seasons", }); - sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); + // sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); } //insert delete episodes //Bulkinsert new data not on db @@ -634,7 +525,7 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { const query = pgp.helpers.insert( episodesToInsert, - columnEpisodes, + jf_library_episodes_columns, "jf_library_episodes" ); await db.query(query); @@ -647,7 +538,8 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_episodes", }); - sendMessageToClients({color:'cornflowerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name}); + insertEpisodeCount += episodesToInsert.length; + // sendMessageToClients({color:'dodgerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name}); } catch (error) { await db.query("ROLLBACK"); message.push({ @@ -656,7 +548,10 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_episodes", }); - sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error}); + sendMessageToClients({ + color: "red", + Message: "Error performing bulk insert:" + error, + }); } })(); } else { @@ -666,7 +561,7 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_episodes", }); - sendMessageToClients({Message:"No new data to bulk insert for " + show.Name}); + // sendMessageToClients({Message:"No new data to bulk insert for " + show.Name}); } //Bulk delete from db thats no longer on api if (existingIdsEpisodes.length > data.allEpisodes.length) { @@ -698,7 +593,9 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_episodes", }); - sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name}); + 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"); @@ -708,7 +605,10 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_episodes", }); - sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error}); + sendMessageToClients({ + color: "red", + Message: "Error performing bulk removal:" + error, + }); } })(); } else { @@ -718,10 +618,29 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { ItemId: show.Id, TableName: "jf_library_episodes", }); - sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); + // sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); } + + sendMessageToClients({ Message: "Sync complete for " + show.Name }); } - sendMessageToClients({color:'lightgreen',Message:"Sync Complete"}); + + 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); console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `); diff --git a/backend/watchdog/ActivityMonitor.js b/backend/watchdog/ActivityMonitor.js new file mode 100644 index 0000000..959497a --- /dev/null +++ b/backend/watchdog/ActivityMonitor.js @@ -0,0 +1,170 @@ +const db = require("../db"); +const pgp = require("pg-promise")(); +const axios = require("axios"); +const { columnsPlayback, mappingPlayback } = require('../models/jf_playback_activity'); +const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require('../models/jf_activity_watchdog'); + +async function ActivityMonitor(interval) { + console.log("Activity Interval: " + interval); + + const { rows: config } = await db.query( + 'SELECT * FROM app_config where "ID"=1' + ); + const base_url = config[0].JF_HOST; + const apiKey = config[0].JF_API_KEY; + + if (base_url === null || config[0].JF_API_KEY === null) { + console.log("Config Details Not Found"); + return; + } + + setInterval(async () => { + try { + const url = `${base_url}/Sessions`; + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": apiKey, + }, + }); + const SessionData=response.data.filter(row => row.NowPlayingItem !== undefined); + + /////get data from jf_activity_monitor + const WatchdogData=await db.query('SELECT * FROM jf_activity_watchdog').then((res) => res.rows); + + // //compare to sessiondata + + let WatchdogDataToInsert = []; + let WatchdogDataToUpdate = []; + //filter fix if table is empty + + if (WatchdogData.length === 0) { + // if there are no existing Ids in the table, map all items in the data array to the expected format + WatchdogDataToInsert = await SessionData.map(jf_activity_watchdog_mapping); + } else { + // otherwise, filter only new data to insert + WatchdogDataToInsert = await SessionData.filter((session) => !WatchdogData.map((wdData) => wdData.Id).includes(session.Id)) + .map(jf_activity_watchdog_mapping); + + WatchdogDataToUpdate = WatchdogData.filter((wdData) => { + const session = SessionData.find((sessionData) => sessionData.Id === wdData.Id); + if (session && session.PlayState) { + if (wdData.IsPaused !== session.PlayState.IsPaused) { + wdData.IsPaused = session.PlayState.IsPaused; + return true; + } + } + return false; + }); + + + } + + // console.log(WatchdogDataToUpdate); + + if (WatchdogDataToInsert.length !== 0) { + db.insertBulk("jf_activity_watchdog",WatchdogDataToInsert,jf_activity_watchdog_columns); + } + + + //update wd state + if(WatchdogDataToUpdate.length>0) + { + + + const WatchdogDataUpdated = WatchdogDataToUpdate.map(obj => { + + const startTime = new Date(obj.ActivityDateInserted); + const endTime =new Date(); + const diffInSeconds = Math.floor((endTime - startTime) / 1000); + + if(obj.IsPaused) { + obj.PlaybackDuration =parseInt(obj.PlaybackDuration)+ diffInSeconds; + } + + obj.ActivityDateInserted = `to_timestamp('${new Date().toISOString()}', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`; + const {...rest } = obj; + + return { ...rest }; + }); + + + + await (async () => { + try { + await db.query("BEGIN"); + const cs = new pgp.helpers.ColumnSet([ + '?Id', + 'IsPaused', + { name: 'PlaybackDuration', mod: ':raw' }, + { name: 'ActivityDateInserted', mod: ':raw' }, + ]); + + const updateQuery = pgp.helpers.update(WatchdogDataUpdated, cs,'jf_activity_watchdog' ) + ' WHERE v."Id" = t."Id"'; + await db.query(updateQuery) + .then(result => { + console.log('Update successful', result.rowCount, 'rows updated'); + }) + .catch(error => { + console.error('Error updating rows', error); + }); + + await db.query("COMMIT"); + } catch (error) { + await db.query("ROLLBACK"); + console.log(error); + } + })(); + } + + //delete from db no longer in session data and insert into stats db (still to make) + //Bulk delete from db thats no longer on api + + const toDeleteIds = WatchdogData.filter((id) =>!SessionData.some((row) => row.Id === id.Id)).map((row) => row.Id); + + + const playbackData = WatchdogData.filter((id) => !SessionData.some((row) => row.Id === id.Id)); + + + const playbackToInsert = playbackData.map(obj => { + + const startTime = new Date(obj.ActivityDateInserted); + const endTime =new Date(); + const diffInSeconds = Math.floor((endTime - startTime) / 1000); + + if(!obj.IsPaused) { + obj.PlaybackDuration =parseInt(obj.PlaybackDuration)+ diffInSeconds; + } + obj.ActivityDateInserted = new Date().toISOString(); + const {...rest } = obj; + + return { ...rest }; + }); + + + + if(toDeleteIds.length>0) + { + let result=await db.deleteBulk('jf_activity_watchdog',toDeleteIds) + console.log(result); + } + if(playbackToInsert.length>0) + { + let result=await db.insertBulk('jf_playback_activity',playbackToInsert,columnsPlayback); + console.log(result); + } + + + /////////////////////////// + + + + } catch (error) { + console.log(error); + return []; + } + }, interval); +} + +module.exports = { + ActivityMonitor, +}; diff --git a/package-lock.json b/package-lock.json index ca0843d..947434e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "axios": "^1.3.4", "concurrently": "^7.6.0", "cors": "^2.8.5", + "http-proxy-middleware": "^2.0.6", "pg": "^8.9.0", "pg-promise": "^11.3.0", "react": "^18.2.0", diff --git a/package.json b/package.json index ad53b37..0ee5f96 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "axios": "^1.3.4", "concurrently": "^7.6.0", "cors": "^2.8.5", + "http-proxy-middleware": "^2.0.6", "pg": "^8.9.0", "pg-promise": "^11.3.0", "react": "^18.2.0", diff --git a/src/App.css b/src/App.css index 5321190..4839435 100644 --- a/src/App.css +++ b/src/App.css @@ -28,34 +28,6 @@ h1{ color: white; } -ul{ - margin: 0; - padding: 0; -} - -li.old { - opacity: 1; - transition: opacity 0.5s ease-in-out; - animation-name: fade-out; - animation-duration: 0.5s; - animation-fill-mode: forwards; - animation-delay: 0s; -} - -li.new { - - border: 2px solid grey; - border-radius: 5px; - padding:10px 10px; - margin:20px 20px 0px 0px; - - opacity: 0; - transition: opacity 1s ease-in-out; - animation-name: fade-in; - animation-duration: 1s; - animation-fill-mode: forwards; - animation-delay: 0s; -} @@ -68,19 +40,6 @@ li.new { to { opacity: 0; } } -.ActivityDetail -{ - font-size: large; - padding-bottom: 5px; -} - -.ActivityTime -{ - /* text-align: right; */ - font-size: small; - font-style: italic; - color: lightgray; -} .App-header { @@ -110,29 +69,3 @@ li.new { } - -.Activity -{ - - /* max-width: 50%; */ - /* border: 1px solid white; */ - border-radius: 5px; - /* margin-left: 50px; - margin-right: 50px; */ - - /* grid-area: "⌛"; */ - - /* background-color: #282c34; */ -} - -.Activity ul -{ - list-style-type: none !important; - color: white; - /* background-color: #282c34; */ -} - /* * - { - outline: 1px solid green; - } - */ diff --git a/src/pages/activity.js b/src/pages/activity.js index 5fdf37d..03ae36e 100644 --- a/src/pages/activity.js +++ b/src/pages/activity.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import API from "../classes/jellyfin-api"; -import "../App.css"; +import "./css/activity.css"; import Loading from "./components/loading"; @@ -12,7 +12,7 @@ function Activity() { let _api = new API(); const fetchData = () => { - _api.getActivityData(30).then((ActivityData) => { + _api.getActivityData(15).then((ActivityData) => { if (data && data.length > 0) { const newDataOnly = ActivityData.Items.filter((item) => { return !data.some((existingItem) => existingItem.Id === item.Id); @@ -46,15 +46,16 @@ function Activity() { } return ( -
-

Activity Log

+
+

Activity

+
    {data && data.map((item) => (
  • items.Id === item.Id) <= 30 + data.findIndex((items) => items.Id === item.Id) <= 15 ? "new" : "old" } @@ -68,6 +69,8 @@ function Activity() {
  • ))}
+
+
); } diff --git a/src/pages/components/ComponentLoading.js b/src/pages/components/ComponentLoading.js new file mode 100644 index 0000000..f3a9ab2 --- /dev/null +++ b/src/pages/components/ComponentLoading.js @@ -0,0 +1,12 @@ +import React from "react"; +import "../css/loading.css"; + +function ComponentLoading() { + return ( +
+
+
+ ); +} + +export default ComponentLoading; \ No newline at end of file diff --git a/src/pages/components/StatsCards.js b/src/pages/components/StatsCards.js new file mode 100644 index 0000000..d3d7a3d --- /dev/null +++ b/src/pages/components/StatsCards.js @@ -0,0 +1,61 @@ +import React, { useState } from "react"; + +import MVLibraries from "./statCards/mv_libraries"; +import MVMovies from "./statCards/mv_movies"; +import MVSeries from "./statCards/mv_series"; +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 "../css/statCard.css"; + +function StatCards() { + const [days, setDays] = useState(30); + const [input, setInput] = useState(30); + + const handleKeyDown = (event) => { + if (event.key === "Enter") { + if (input < 1) { + setInput(1); + setDays(0); + } else { + setDays(parseInt(input) - 1); + } + + console.log(days); + } + }; + return ( +
+
+

Watch Statistics

+
+
Last
+
+ setInput(event.target.value)} + onKeyDown={handleKeyDown} + /> +
+
Days
+
+ + +
+
+ + + + + + + +
+
+ ); +} + +export default StatCards; diff --git a/src/pages/components/libraryOverview.js b/src/pages/components/libraryOverview.js index 46a2167..12754fe 100644 --- a/src/pages/components/libraryOverview.js +++ b/src/pages/components/libraryOverview.js @@ -4,22 +4,26 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; import Loading from "./loading"; + +import TvLineIcon from "remixicon-react/TvLineIcon"; +import FilmLineIcon from "remixicon-react/FilmLineIcon"; + export default function LibraryOverView() { const [data, setData] = useState([]); const [base_url, setURL] = useState(""); useEffect(() => { if (base_url === "") { - Config() - .then((config) => { - setURL(config.hostUrl); - }) - .catch((error) => { - console.log(error); - }); - } + Config() + .then((config) => { + setURL(config.hostUrl); + }) + .catch((error) => { + console.log(error); + }); + } const fetchData = () => { - const url = `http://localhost:3003/stats/getLibraryOverview`; + const url = `/stats/getLibraryOverview`; axios .get(url) .then((response) => setData(response.data)) @@ -29,48 +33,77 @@ export default function LibraryOverView() { if (!data || data.length === 0) { fetchData(); } - - }, [data,base_url]); + }, [data, base_url]); if (data.length === 0) { return ; } return ( -
- {data && - data.map((stats) => ( -
-
-
-

Items in Library

{stats.Library_Count}

-
- {stats.CollectionType === "tvshows" ? ( -
-

Seasons

{stats.Season_Count}

-
- ) : ( - <> - )} - {stats.CollectionType === "tvshows" ? ( -
-

Episodes

{stats.Episode_Count}

-
- ) : ( - <> - )} +
+

Library Statistics

+
+
+ +
+
+
- ))} + +
+
+
MOVIE LIBRARIES
+
MOVIES
+
+ +
+ {data && + data.filter((stat) => stat.CollectionType === "movies") + .map((item, index) => ( +
+

{index + 1}

+

{item.Name}

+

{item.Library_Count}

+
+ ))} +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
SHOW LIBRARIES
+
+ SERIES / SEASONS / EPISODES +
+
+ +
+ {data && + data.filter((stat) => stat.CollectionType === "tvshows") + .map((item, index) => ( +
+

{index + 1}

+

{item.Name}

+

{item.Library_Count} / {item.Season_Count} / {item.Episode_Count}

+
+ ))} +
+
+
+ +
+
); } diff --git a/src/pages/components/playbackactivity.js b/src/pages/components/playbackactivity.js new file mode 100644 index 0000000..e7c1511 --- /dev/null +++ b/src/pages/components/playbackactivity.js @@ -0,0 +1,125 @@ +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 "./loading"; + +function PlaybackActivity() { + 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 = () => { + + axios + .get('/stats/getPlaybackActivity') + .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, 10000); + return () => clearInterval(intervalId); + }, [data]); + + function convertSecondsToHMS(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + return `${hours}h ${minutes}m ${remainingSeconds}s`; + } + const options = { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, + }; + + if (!data || data.length === 0) { + return ; + } + const sortedData = sortData(data, sortConfig); + return ( +
+

Playback Activity

+ + + + + + + + + + + + {sortedData.map((item) => ( + + + + + + + + ))} + +
handleSort("UserName")}>User handleSort("NowPlayingItemName")}>Watched handleSort("NowPlayingItemName")}>Episode handleSort("PlaybackDuration")}>Playback Duration handleSort("ActivityDateInserted")}>Playback Timestamp
{item.UserName}{item.SeriesName || item.NowPlayingItemName}{item.SeriesName ? item.NowPlayingItemName: '' }{convertSecondsToHMS(item.PlaybackDuration)}{new Date(item.ActivityDateInserted).toLocaleString("en-GB", options)}
+
+ ); +} + +export default PlaybackActivity; diff --git a/src/pages/components/session-card.js b/src/pages/components/session-card.js index 8e263b4..579b801 100644 --- a/src/pages/components/session-card.js +++ b/src/pages/components/session-card.js @@ -98,7 +98,7 @@ function sessionCard(props) { return (
{ _api.getSessions().then((SessionData) => { - setData(SessionData); + let results=SessionData.filter((session) => session.NowPlayingItem); + setData(results); + }); }; @@ -35,22 +37,34 @@ function Sessions() { const intervalId = setInterval(fetchData, 1000); return () => clearInterval(intervalId); - }, []); + }, [base_url]); - if (!data || data.length === 0) { + if (!data) { return ; } + if (data.length === 0) { + return(
+

Sessions

+
+ No Active Sessions Found +
+
); + } + return ( -
- {data && - data - .sort((a, b) => - a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0")) - ) - .map((session) => ( - - ))} +
+

Sessions

+
+ {data && + data + .sort((a, b) => + a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0")) + ) + .map((session) => ( + + ))} +
); } diff --git a/src/pages/components/settings/WebSocketComponent .js b/src/pages/components/settings/WebSocketComponent .js index 2cd84e8..0ff3e35 100644 --- a/src/pages/components/settings/WebSocketComponent .js +++ b/src/pages/components/settings/WebSocketComponent .js @@ -7,7 +7,7 @@ const WebSocketComponent = () => { useEffect(() => { // create a new WebSocket connection - const socket = new WebSocket('ws://localhost:8080'); + const socket = new WebSocket('ws://10.0.0.20:8080'); // handle incoming messages socket.addEventListener('message', (event) => { @@ -33,7 +33,7 @@ const WebSocketComponent = () => { return (
- {/*

WebSocket Example

*/} +

Terminal

{messages.map((message, index) => (
diff --git a/src/pages/components/settings/librarySync.js b/src/pages/components/settings/librarySync.js index 278823a..7e3bc47 100644 --- a/src/pages/components/settings/librarySync.js +++ b/src/pages/components/settings/librarySync.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import axios from "axios"; // import Config from "../../../lib/config"; // import Loading from "../loading"; @@ -6,33 +6,47 @@ import axios from "axios"; import "../../css/settings.css"; export default function LibrarySync() { - + const [processing, setProcessing] = useState(false); async function writeSeasonsAndEpisodes() { - // Send a GET request to /system/configuration to test copnnection - let isValid = false; - let errorMessage = ""; + + + setProcessing(true); + await axios - .get("http://localhost:3003/sync/writeSeasonsAndEpisodes") + .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; + // isValid = true; } }) .catch((error) => { console.log(error); }); - - return { isValid: isValid, errorMessage: errorMessage }; + setProcessing(false); + // return { isValid: isValid, errorMessage: errorMessage }; } const handleClick = () => { + writeSeasonsAndEpisodes(); console.log('Button clicked!'); } return ( -
- +
+
); diff --git a/src/pages/components/settings/settingsConfig.js b/src/pages/components/settings/settingsConfig.js index 2b86db8..d59f81f 100644 --- a/src/pages/components/settings/settingsConfig.js +++ b/src/pages/components/settings/settingsConfig.js @@ -79,7 +79,7 @@ export default function SettingsConfig() { // Send a POST request to /api/setconfig/ with the updated configuration axios - .post("http://localhost:3003/api/setconfig/", formValues, { + .post("/api/setconfig/", formValues, { headers: { "Content-Type": "application/json", }, diff --git a/src/pages/components/statCards/most_active_users.js b/src/pages/components/statCards/most_active_users.js new file mode 100644 index 0000000..3a5b600 --- /dev/null +++ b/src/pages/components/statCards/most_active_users.js @@ -0,0 +1,127 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + + +import ComponentLoading from "../ComponentLoading"; + +import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon"; + +function MostActiveUsers() { + const [data, setData] = useState([]); + const [imgError, setImgError] = useState(false); + + 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/getMostActiveUsers`; + + axios + .get(url) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + + if (!config) { + fetchConfig(); + } + + if (!data || data.length===0) { + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, config]); + + + + const handleImageError = () => { + setImgError(true); + }; + + if (!data) { + return( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + return ( +
+ +
+ {imgError ? + + + : + + + } +
+
+
+ +
MOST ACTIVE USERS
+
Plays
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.UserName}

+

{item.Plays}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MostActiveUsers; diff --git a/src/pages/components/statCards/most_used_client.js b/src/pages/components/statCards/most_used_client.js new file mode 100644 index 0000000..863efc5 --- /dev/null +++ b/src/pages/components/statCards/most_used_client.js @@ -0,0 +1,107 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + + +import ComponentLoading from "../ComponentLoading"; + +import ComputerLineIcon from "remixicon-react/ComputerLineIcon"; + +function MostUsedClient() { + const [data, setData] = useState([]); +// 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/getMostUsedClient`; + + axios + .get(url) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + + if (!config) { + fetchConfig(); + } + + if (!data || data.length===0) { + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, config]); + + if (!data) { + return( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + return ( +
+ +
+
+ +
+
+
+
+ +
MOST USED CLIENTS
+
Plays
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.Client}

+

{item.Plays}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MostUsedClient; diff --git a/src/pages/components/statCards/mp_movies.js b/src/pages/components/statCards/mp_movies.js new file mode 100644 index 0000000..590f129 --- /dev/null +++ b/src/pages/components/statCards/mp_movies.js @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ComponentLoading from "../ComponentLoading"; + +// import PlaybackActivity from "./components/playbackactivity"; + +function MPMovies(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/getMostPopularMovies`; + + 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( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + return ( +
+ +
+ + +
+
+
+ +
MOST POPULAR MOVIES
+
Users
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.Name}

+

{item.unique_viewers}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MPMovies; diff --git a/src/pages/components/statCards/mp_series.js b/src/pages/components/statCards/mp_series.js new file mode 100644 index 0000000..816d796 --- /dev/null +++ b/src/pages/components/statCards/mp_series.js @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ComponentLoading from "../ComponentLoading"; + +// import PlaybackActivity from "./components/playbackactivity"; + +function MPSeries(props) { + const [data, setData] = useState([]); + const [days, setDays] = useState(30); +// const [base_url, setURL] = useState(""); + + const [config, setConfig] = useState(null); + + console.log('PROPS: '+ days); + 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/getMostPopularSeries`; + + 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( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + + return ( +
+ +
+ + +
+
+
+ +
MOST POPULAR SERIES
+
Users
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.Name}

+

{item.unique_viewers}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MPSeries; diff --git a/src/pages/components/statCards/mv_libraries.js b/src/pages/components/statCards/mv_libraries.js new file mode 100644 index 0000000..3bea621 --- /dev/null +++ b/src/pages/components/statCards/mv_libraries.js @@ -0,0 +1,121 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + + +import ComponentLoading from "../ComponentLoading"; + +import TvLineIcon from "remixicon-react/TvLineIcon"; +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 + .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( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + return ( +
+ +
+
+ {data[0].CollectionType==="tvshows" ? + + + : + + } + +
+
+
+
+ +
MOST VIEWED LIBRARIES
+
Plays
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.Name}

+

{item.Plays}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MVLibraries; diff --git a/src/pages/components/statCards/mv_movies.js b/src/pages/components/statCards/mv_movies.js new file mode 100644 index 0000000..a0d9c73 --- /dev/null +++ b/src/pages/components/statCards/mv_movies.js @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ComponentLoading from "../ComponentLoading"; + + +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/getMostViewedMovies`; + + 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( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + + return ( +
+ +
+ + +
+
+
+ +
MOST VIEWED MOVIES
+
Plays
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.Name}

+

{item.Plays}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MVMovies; diff --git a/src/pages/components/statCards/mv_series.js b/src/pages/components/statCards/mv_series.js new file mode 100644 index 0000000..ecda9cd --- /dev/null +++ b/src/pages/components/statCards/mv_series.js @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ComponentLoading from "../ComponentLoading"; + +// import PlaybackActivity from "./components/playbackactivity"; + +function MVSeries(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/getMostViewedSeries`; + + 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( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + return ( +
+ +
+ + +
+
+
+ +
MOST VIEWED SERIES
+
Plays
+
+ +
+ + {data && + data + .map((item,index) => ( + +
+

{(index+1)}

+

{item.Name}

+

{item.Plays}

+
+ + ))} + +
+
+ + +
+ ); +} + +export default MVSeries; diff --git a/src/pages/css/activity.css b/src/pages/css/activity.css new file mode 100644 index 0000000..005dd2f --- /dev/null +++ b/src/pages/css/activity.css @@ -0,0 +1,57 @@ + +.Activity +{ + border-radius: 5px; +} + +.Activity ul +{ + list-style-type: none !important; + color: white; + width: fit-content; + +} + +.ActivityDetail +{ + font-size: 1em; + padding-bottom: 5px; +} + +.ActivityTime +{ + /* text-align: right; */ + font-size: small; + font-style: italic; + color: lightgray; +} +ul{ + margin: 0; + padding: 0; + } + + li.old { + opacity: 1; + transition: opacity 0.5s ease-in-out; + animation-name: fade-out; + animation-duration: 0.5s; + animation-fill-mode: forwards; + animation-delay: 0s; + } + + li.new { + + /* border: 2px solid grey; */ + border-radius: 5px; + padding:10px 10px; + margin:10px 10px 0px 0px; + + opacity: 0; + transition: opacity 1s ease-in-out; + animation-name: fade-in; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-delay: 0s; + background-color: rgba(0, 0, 0, 0.4); + } + \ No newline at end of file diff --git a/src/pages/css/libraryOverview.css b/src/pages/css/libraryOverview.css index 9404441..5812df2 100644 --- a/src/pages/css/libraryOverview.css +++ b/src/pages/css/libraryOverview.css @@ -1,62 +1,113 @@ -.overview { - color: white; - margin-top: 20px; - font-family: 'Railway', sans-serif; - font-weight: bold; +.overview-container +{ + display: grid; + grid-template-columns: repeat(auto-fit, minmax(520px, 520px)); + grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ + + border-radius: 4px; + margin-right: 20px; +} + +.library-card +{ + width: 500px; + height: 180px; display: flex; - flex-direction: row; + color: white; + /* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.5); */ + background: linear-gradient(to right, #00A4DC, #AA5CC3); + background-size: cover; +} + + + +.library-icons +{ + display: flex; + justify-content: center; + align-items: center; + height: 100%; + +} + + .library-header { + display: flex; + justify-content: space-between; + color: white; + font-weight: 500; + } + .library-header-count { + color: lightgray; + font-weight: 300; + + } + + .library-item { + display: flex; + justify-content: space-between; + width: 100%; + height: 20px; + margin-bottom: 5px; } - .card { + .library-item-index { + padding-top: 3px; + font-size: 0.8em; + padding-right: 2px; + color: grey; + + text-align: right; + } + .library-item-name { + width: 35%; + } + + .library-item-count { + width: 60%; + text-align: right; + color: #00A4DC; + font-weight: 500; + font-size: 1.1em; + + } + + .library-image + { + display: flex; + justify-content: center; + align-items: center; + height: 180px; + width: 180px; + background-color: rgb(0, 0, 0, 0.6); + } + + .library-banner-image + { + + + height: 180px; + width: 120px; + + + } + + + .library-user-image + { + + border-radius: 50%; + width: 80%; + object-fit: cover; + + } + + + .library{ width: 100%; - height: 200px; - margin-right: 20px; - border-radius: 4px; - text-align: center; - background-size: cover; - background-position: center; - margin-bottom: 5px; - background-color: black; - } - - .item-card-count { - - padding-top: 20px; - padding-bottom: 10px; + padding: 5px 20px; + backdrop-filter: blur(4px); + + background-color: rgb(0, 0, 0, 0.6); } - .item-count { - backdrop-filter: blur(2px); - font-size: 20px; - display: flex; - justify-content: center; - align-items: center; - height: 100%; - padding-left: 30px; - padding-right: 30px; - } - - .item-count > div { - display: inline-block; - margin-right: 10px; - text-align: center; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - .item-count > div p { - margin: 0; - } - - .item-count > div:not(:last-child) { - margin-right: 10px; - } - - p { - margin: 0; - } - \ No newline at end of file diff --git a/src/pages/css/loading.css b/src/pages/css/loading.css index c4608c4..b32a740 100644 --- a/src/pages/css/loading.css +++ b/src/pages/css/loading.css @@ -7,7 +7,16 @@ display: flex; justify-content: center; align-items: center; - /* background-color: rgba(255, 255, 255, 0.8); */ + z-index: 9999; + } + .component-loading { + + height: inherit; + width: inherit; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.8); z-index: 9999; } diff --git a/src/pages/css/sessions.css b/src/pages/css/sessions.css index da342cc..306d12d 100644 --- a/src/pages/css/sessions.css +++ b/src/pages/css/sessions.css @@ -2,9 +2,9 @@ display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 520px)); grid-auto-rows: 235px;/* max-width+offset so 215 + 20*/ - background-color: rgba(0,0,0,0.5); - padding: 20px; - border-radius: 4px; + /* background-color: rgba(0,0,0,0.5); */ + /* padding: 20px; */ + /* border-radius: 4px; */ margin-right: 20px; } @@ -15,7 +15,7 @@ color: white; background-color: grey; - box-shadow: 0 0 20px rgba(255, 255, 255, 0.05); + /* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */ max-height: 215px; max-width: 500px; @@ -24,7 +24,7 @@ /* margin-bottom: 10px; */ background-size: cover; - border-radius: 4px 4px 0px 4px; + /* border-radius: 4px 4px 0px 4px; */ display: grid; grid-template-columns: auto 1fr; @@ -41,14 +41,14 @@ grid-column: 1/3; height: 5px; background-color: #101010; - border-radius: 0px 0px 4px 4px; + /* border-radius: 0px 0px 4px 4px; */ } .progress { height: 100%; background-color: #00A4DC; transition: width 0.2s ease-in-out; - border-radius: 0px 0px 0px 4px; + /* border-radius: 0px 0px 0px 4px; */ } .card-banner { @@ -76,8 +76,9 @@ .card-banner-image { - border-radius: 4px 0px 0px 0px; + /* border-radius: 4px 0px 0px 0px; */ max-height: inherit; + /* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */ } .card-user { diff --git a/src/pages/css/settings.css b/src/pages/css/settings.css index baeb826..6cd9481 100644 --- a/src/pages/css/settings.css +++ b/src/pages/css/settings.css @@ -31,6 +31,7 @@ border: none; cursor: pointer; transition: all 0.3s ease-in-out; + margin-bottom: 10px; } .settings-form button:hover { diff --git a/src/pages/css/statCard.css b/src/pages/css/statCard.css new file mode 100644 index 0000000..e0d7e62 --- /dev/null +++ b/src/pages/css/statCard.css @@ -0,0 +1,170 @@ +.Heading +{ + display: flex; +} + +.Heading h1 +{ + padding-right: 10px; +} + +.stat-cards-container +{ + display: grid; + grid-template-columns: repeat(auto-fit, minmax(520px, 520px)); + grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ + + border-radius: 4px; + margin-right: 20px; + margin-top: 4px; +} + +.stats-card +{ + width: 500px; + height: 180px; + display: flex; + color: white; + /* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.5); */ + background: linear-gradient(to right, #00A4DC, #AA5CC3); + background-size: cover; +} + + + +.library-icons +{ + display: flex; + justify-content: center; + align-items: center; + height: 100%; + +} + + .stats-header { + display: flex; + justify-content: space-between; + color: white; + font-weight: 500; + } + .stats-header-plays { + color: lightgray; + font-weight: 300; + } + + .stat-item { + display: flex; + justify-content: space-between; + width: 100%; + height: 20px; + margin-bottom: 5px; + + } + + .stat-item-index { + padding-top: 6px; + font-size: 0.8em; + padding-right: 2px; + color: grey; + + text-align: right; + } + .stat-item-name { + width: 85%; + } + + .stat-item-count { + width: 10%; + text-align: right; + color: #00A4DC; + font-weight: 500; + font-size: 1.1em; + + } + + .popular-image + { + display: flex; + justify-content: center; + align-items: center; + height: 180px; + width: 175px; + background-color: rgb(0, 0, 0, 0.6); + } + + .popular-banner-image + { + + + height: 180px; + width: 120px; + + + } + + + .popular-user-image + { + + border-radius: 50%; + width: 80%; + object-fit: cover; + + } + + + .stats{ + width: 100%; + padding: 5px 20px; + backdrop-filter: blur(4px); + + background-color: rgb(0, 0, 0, 0.6); + } + +.date-range +{ + width: 220px; + height: 35px; + color: white; + display: flex; + background-color: rgb(0, 99, 248,0.6); + border-radius: 4px; + font-size: 1.2em; + align-self: center; +} + + +.date-range .days input +{ + height: 35px; + outline: none; + border: none; + background-color:transparent; + color:white; + font-size: 1em; + width: 40px; +} +.date-range .days +{ + background-color: rgb(255, 255, 255, 0.1); + padding-inline: 10px; +} + + +input[type=number]::-webkit-outer-spin-button, +input[type=number]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type=number] { + -moz-appearance: textfield; +} + + +.date-range .header, +.date-range .trailer +{ + padding: 5px; + text-align: center; +} diff --git a/src/pages/css/usersactivity.css b/src/pages/css/usersactivity.css index fbd3241..9e8744a 100644 --- a/src/pages/css/usersactivity.css +++ b/src/pages/css/usersactivity.css @@ -13,7 +13,7 @@ font-family: sans-serif; min-width: 400px; box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); - + color: white; width: 100%; } @@ -22,7 +22,9 @@ td { padding: 12px 15px; /* text-align: left; */ - border-bottom: 1px solid rgba(255, 255, 255, 0.15); + 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 { @@ -36,9 +38,9 @@ th:hover { -tbody tr:last-of-type { +/* tbody tr:last-of-type { border-bottom: 2px solid #009879; -} +} */ .card-user-image @@ -50,5 +52,16 @@ tbody tr:last-of-type { } +tr:hover +{ + background-color: rgba(0, 0, 0, 0.2); +} +td:first-child { + border-left: none; + } + + td:last-child { + border-right: none; + } diff --git a/src/pages/home.js b/src/pages/home.js index b3c9a13..eb09af3 100644 --- a/src/pages/home.js +++ b/src/pages/home.js @@ -3,14 +3,18 @@ import React from 'react' import './css/home.css' import Sessions from './components/sessions' +import StatCards from './components/StatsCards' import LibraryOverView from './components/libraryOverview' + export default function Home() { return (
- -

Sessions

+ + + +
) } \ No newline at end of file diff --git a/src/pages/libraries.js b/src/pages/libraries.js index cd974ba..fdc7b9e 100644 --- a/src/pages/libraries.js +++ b/src/pages/libraries.js @@ -3,13 +3,42 @@ import axios from "axios"; import Config from "../lib/config"; import "./css/libraries.css"; +import "./css/usersactivity.css"; import Loading from "./components/loading"; +// import PlaybackActivity from "./components/playbackactivity"; + function Libraries() { const [data, setData] = useState([]); + const [items, setItems] = useState([]); const [config, setConfig] = useState(null); + async function fetchLibraryData(libraryId) { + console.log("data: "+libraryId); + if (config) { + const url = `/api/getLibraryItems`; + await axios + .post(url, {}, { + headers: { + "id": libraryId, + } + }) + .then((response) => { + console.log("data"); + setItems(response.data); + console.log(response); + }) + .catch((error) => { + console.log(error); + }); + } + + + + + } + useEffect(() => { const fetchConfig = async () => { try { @@ -22,7 +51,7 @@ function Libraries() { } }; - const fetchData = () => { + const fetchLibraries = () => { if (config) { const url = `${config.hostUrl}/Library/MediaFolders`; const apiKey = config.apiKey; @@ -43,22 +72,28 @@ function Libraries() { }); } }; + if (!config) { fetchConfig(); } if (data.length === 0) { - fetchData(); + fetchLibraries(); } - const intervalId = setInterval(fetchData, 60000 * 60); + const intervalId = setInterval(fetchLibraries, 60000 * 60); return () => clearInterval(intervalId); }, [data, config]); if (!data || data.length === 0) { return ; } + const handleClick = (event) => { + fetchLibraryData(event.target.value); + console.log(event.target.value); + // console.log('Button clicked!'); + } return (
@@ -71,22 +106,42 @@ function Libraries() { ) .map((item) => (
  • - {/*
    {item.Name}
    */} -
    - -
    +
    {item.Name}
    + +
  • ))} +

    Library Data

    + + + + + + + + + + + {items.map((item) => ( + + + + + + ))} + +
    IdNameType
    {item.Id}{item.Name}{item.Type}
    + {/*
      + {items && + items.map((item) => ( +
    • +

      {item.Name}

      +

      {item.Id}

      + +
    • + ))} +
    */}
    ); } diff --git a/src/pages/setup.js b/src/pages/setup.js index 700896d..2a97ed1 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -68,7 +68,7 @@ function Setup() { // Send a POST request to /api/setconfig/ with the updated configuration axios - .post("http://localhost:3003/api/setconfig/", formValues, { + .post("/api/setconfig/", formValues, { headers: { "Content-Type": "application/json", }, diff --git a/src/pages/userdata.js b/src/pages/userdata.js index a789c2b..740ebab 100644 --- a/src/pages/userdata.js +++ b/src/pages/userdata.js @@ -3,8 +3,15 @@ import React, { useState, useEffect } from 'react'; import './css/libraries.css'; import Loading from './components/loading'; +// import PlaybackActivity from './components/playbackactivity'; + +// import StatCards from './components/StatsCards'; + +import LibraryOverView from './components/libraryOverview'; + import API from '../classes/jellyfin-api'; + function UserData() { const [data, setData] = useState([]); @@ -26,15 +33,19 @@ function UserData() { return (
    -

    Libraries

    + {/*

    Libraries

      {data.map((series) => (
    • {series.Name}
    • ))} -
    + */} +{/* */} + +
    + ); } diff --git a/src/setupProxy.js b/src/setupProxy.js new file mode 100644 index 0000000..7785242 --- /dev/null +++ b/src/setupProxy.js @@ -0,0 +1,26 @@ +const { createProxyMiddleware } = require('http-proxy-middleware'); + +module.exports = function(app) { + app.use( + '/api', + createProxyMiddleware({ + target: 'http://localhost:3003', + changeOrigin: true, + }) + ); + app.use( + '/stats', + createProxyMiddleware({ + target: 'http://localhost:3003', + changeOrigin: true, + }) + ); + app.use( + '/sync', + createProxyMiddleware({ + target: 'http://localhost:3003', + changeOrigin: true, + }) + ); + console.log('Proxy middleware applied to /api'); +};