diff --git a/SQL Scripts/1. CREATE DATABASE jfstat.sql b/SQL Scripts/1. CREATE DATABASE jfstat.sql deleted file mode 100644 index 4cda4db..0000000 --- a/SQL Scripts/1. CREATE DATABASE jfstat.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Database: jfstat - --- DROP DATABASE IF EXISTS jfstat; - -CREATE DATABASE jfstat - WITH - OWNER = jfstat - ENCODING = 'UTF8' - LC_COLLATE = 'en_US.utf8' - LC_CTYPE = 'en_US.utf8' - TABLESPACE = pg_default - CONNECTION LIMIT = -1 - IS_TEMPLATE = False; \ 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 deleted file mode 100644 index ac5ebc3..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_activity_watchdog.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Table: public.jf_activity_watchdog - --- DROP TABLE IF EXISTS public.jf_activity_watchdog; - -CREATE TABLE IF NOT EXISTS public.jf_activity_watchdog -( - "Id" text COLLATE pg_catalog."default" NOT NULL, - "IsPaused" boolean DEFAULT false, - "UserId" text COLLATE pg_catalog."default", - "UserName" text COLLATE pg_catalog."default", - "Client" text COLLATE pg_catalog."default", - "DeviceName" text COLLATE pg_catalog."default", - "DeviceId" text COLLATE pg_catalog."default", - "ApplicationVersion" text COLLATE pg_catalog."default", - "NowPlayingItemId" text COLLATE pg_catalog."default", - "NowPlayingItemName" text COLLATE pg_catalog."default", - "SeasonId" text COLLATE pg_catalog."default", - "SeriesName" text COLLATE pg_catalog."default", - "EpisodeId" text COLLATE pg_catalog."default", - "PlaybackDuration" bigint, - "ActivityDateInserted" timestamp with time zone -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.jf_activity_watchdog - OWNER to postgres; \ 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 deleted file mode 100644 index f88907b..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_episodes.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Table: public.jf_library_episodes - --- DROP TABLE IF EXISTS public.jf_library_episodes; - -CREATE TABLE IF NOT EXISTS public.jf_library_episodes -( - "Id" text COLLATE pg_catalog."default" NOT NULL, - "EpisodeId" text COLLATE pg_catalog."default" NOT NULL, - "Name" text COLLATE pg_catalog."default", - "ServerId" text COLLATE pg_catalog."default", - "PremiereDate" timestamp with time zone, - "OfficialRating" text COLLATE pg_catalog."default", - "CommunityRating" double precision, - "RunTimeTicks" bigint, - "ProductionYear" integer, - "IndexNumber" integer, - "ParentIndexNumber" integer, - "Type" text COLLATE pg_catalog."default", - "ParentLogoItemId" text COLLATE pg_catalog."default", - "ParentBackdropItemId" text COLLATE pg_catalog."default", - "ParentBackdropImageTags" text COLLATE pg_catalog."default", - "SeriesId" text COLLATE pg_catalog."default", - "SeasonId" text COLLATE pg_catalog."default", - "SeasonName" text COLLATE pg_catalog."default", - "SeriesName" text COLLATE pg_catalog."default", - CONSTRAINT jf_library_episodes_pkey PRIMARY KEY ("Id") -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.jf_library_episodes - OWNER to postgres; \ 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 deleted file mode 100644 index 1e65126..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Table: public.app_config - --- DROP TABLE IF EXISTS public.app_config; - -CREATE TABLE IF NOT EXISTS public.app_config -( - "ID" integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ), - "JF_HOST" text COLLATE pg_catalog."default", - "JF_API_KEY" text COLLATE pg_catalog."default", - "APP_USER" text COLLATE pg_catalog."default", - "APP_PASSWORD" text COLLATE pg_catalog."default", - CONSTRAINT app_config_pkey PRIMARY KEY ("ID") -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.app_config - OWNER to postgres; \ 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 deleted file mode 100644 index bca0452..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Table: public.jf_libraries - --- DROP TABLE IF EXISTS public.jf_libraries; - -CREATE TABLE IF NOT EXISTS public.jf_libraries -( - "Id" text COLLATE pg_catalog."default" NOT NULL, - "Name" text COLLATE pg_catalog."default" NOT NULL, - "ServerId" text COLLATE pg_catalog."default", - "IsFolder" boolean NOT NULL DEFAULT true, - "Type" text COLLATE pg_catalog."default" NOT NULL DEFAULT 'CollectionFolder'::text, - "CollectionType" text COLLATE pg_catalog."default" NOT NULL, - "ImageTagsPrimary" text COLLATE pg_catalog."default", - CONSTRAINT jf_libraries_pkey PRIMARY KEY ("Id") -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.jf_libraries - OWNER to postgres; \ 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 deleted file mode 100644 index 9c37393..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_items.sql +++ /dev/null @@ -1,38 +0,0 @@ --- Table: public.jf_library_items - --- DROP TABLE IF EXISTS public.jf_library_items; - -CREATE TABLE IF NOT EXISTS public.jf_library_items -( - "Id" text COLLATE pg_catalog."default" NOT NULL, - "Name" text COLLATE pg_catalog."default" NOT NULL, - "ServerId" text COLLATE pg_catalog."default", - "PremiereDate" timestamp with time zone, - "EndDate" timestamp with time zone, - "CommunityRating" double precision, - "RunTimeTicks" bigint, - "ProductionYear" integer, - "IsFolder" boolean, - "Type" text COLLATE pg_catalog."default", - "Status" text COLLATE pg_catalog."default", - "ImageTagsPrimary" text COLLATE pg_catalog."default", - "ImageTagsBanner" text COLLATE pg_catalog."default", - "ImageTagsLogo" text COLLATE pg_catalog."default", - "ImageTagsThumb" text COLLATE pg_catalog."default", - "BackdropImageTags" text COLLATE pg_catalog."default", - "ParentId" text COLLATE pg_catalog."default" NOT NULL, - CONSTRAINT jf_library_items_pkey PRIMARY KEY ("Id"), - CONSTRAINT jf_library_items_fkey FOREIGN KEY ("ParentId") - REFERENCES public.jf_libraries ("Id") MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE SET NULL - NOT VALID -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.jf_library_items - OWNER to postgres; - -COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items - IS 'jf_library'; \ 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 deleted file mode 100644 index 63d7eaa..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_library_seasons.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Table: public.jf_library_seasons - --- DROP TABLE IF EXISTS public.jf_library_seasons; - -CREATE TABLE IF NOT EXISTS public.jf_library_seasons -( - "Id" text COLLATE pg_catalog."default" NOT NULL, - "Name" text COLLATE pg_catalog."default", - "ServerId" text COLLATE pg_catalog."default", - "IndexNumber" integer, - "Type" text COLLATE pg_catalog."default", - "ParentLogoItemId" text COLLATE pg_catalog."default", - "ParentBackdropItemId" text COLLATE pg_catalog."default", - "ParentBackdropImageTags" text COLLATE pg_catalog."default", - "SeriesName" text COLLATE pg_catalog."default", - "SeriesId" text COLLATE pg_catalog."default", - "SeriesPrimaryImageTag" text COLLATE pg_catalog."default", - CONSTRAINT jf_library_seasons_pkey PRIMARY KEY ("Id") -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.jf_library_seasons - OWNER to postgres; \ 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 deleted file mode 100644 index 5826794..0000000 --- a/SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_playback_activity.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Table: public.jf_playback_activity - --- DROP TABLE IF EXISTS public.jf_playback_activity; - -CREATE TABLE IF NOT EXISTS public.jf_playback_activity -( - "Id" text COLLATE pg_catalog."default" NOT NULL, - "IsPaused" boolean DEFAULT false, - "UserId" text COLLATE pg_catalog."default", - "UserName" text COLLATE pg_catalog."default", - "Client" text COLLATE pg_catalog."default", - "DeviceName" text COLLATE pg_catalog."default", - "DeviceId" text COLLATE pg_catalog."default", - "ApplicationVersion" text COLLATE pg_catalog."default", - "NowPlayingItemId" text COLLATE pg_catalog."default", - "NowPlayingItemName" text COLLATE pg_catalog."default", - "SeasonId" text COLLATE pg_catalog."default", - "SeriesName" text COLLATE pg_catalog."default", - "EpisodeId" text COLLATE pg_catalog."default", - "PlaybackDuration" bigint, - "ActivityDateInserted" timestamp with time zone -) - -TABLESPACE pg_default; - -ALTER TABLE IF EXISTS public.jf_playback_activity - OWNER to postgres; \ 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 deleted file mode 100644 index bb4a552..0000000 --- a/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_played_items.sql +++ /dev/null @@ -1,45 +0,0 @@ --- FUNCTION: public.fs_most_played_items(integer, text) - --- DROP FUNCTION IF EXISTS public.fs_most_played_items(integer, text); - -CREATE OR REPLACE FUNCTION public.fs_most_played_items( - days integer, - itemtype text) - RETURNS TABLE("Plays" bigint, total_playback_duration numeric, "Name" text, "Id" text) - LANGUAGE 'plpgsql' - COST 100 - VOLATILE PARALLEL UNSAFE - ROWS 1000 - -AS $BODY$ -BEGIN - RETURN QUERY - SELECT - t.plays, - t.total_playback_duration, - i."Name", - i."Id" - FROM ( - SELECT - count(*) AS plays, - sum(jf_playback_activity."PlaybackDuration") AS total_playback_duration, - jf_playback_activity."NowPlayingItemId" - FROM - jf_playback_activity - WHERE - jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW() - GROUP BY - jf_playback_activity."NowPlayingItemId" - ORDER BY - count(*) DESC - ) t - JOIN jf_library_items i - ON t."NowPlayingItemId" = i."Id" - AND i."Type" = itemType - ORDER BY - t.plays DESC; -END; -$BODY$; - -ALTER FUNCTION public.fs_most_played_items(integer, text) - OWNER TO postgres; 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 deleted file mode 100644 index 54cf83a..0000000 --- a/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_popular_items.sql +++ /dev/null @@ -1,52 +0,0 @@ --- FUNCTION: public.fs_most_popular_items(integer, text) - --- DROP FUNCTION IF EXISTS public.fs_most_popular_items(integer, text); - -CREATE OR REPLACE FUNCTION public.fs_most_popular_items( - days integer, - itemtype text) - RETURNS TABLE(unique_viewers bigint, latest_activity_date timestamp with time zone, "Name" text, "Id" text) - LANGUAGE 'plpgsql' - COST 100 - VOLATILE PARALLEL UNSAFE - ROWS 1000 - -AS $BODY$ -BEGIN - RETURN QUERY - SELECT - t.unique_viewers, - t.latest_activity_date, - i."Name", - i."Id" - FROM ( - SELECT - jf_playback_activity."NowPlayingItemId", - count(DISTINCT jf_playback_activity."UserId") AS unique_viewers, - latest_activity_date.latest_date AS latest_activity_date - FROM - jf_playback_activity - JOIN ( - SELECT - jf_playback_activity_1."NowPlayingItemId", - max(jf_playback_activity_1."ActivityDateInserted") AS latest_date - FROM - jf_playback_activity jf_playback_activity_1 - GROUP BY jf_playback_activity_1."NowPlayingItemId" - ) latest_activity_date - ON jf_playback_activity."NowPlayingItemId" = latest_activity_date."NowPlayingItemId" - WHERE - jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW() - GROUP BY - jf_playback_activity."NowPlayingItemId", latest_activity_date.latest_date - ) t - JOIN jf_library_items i - ON t."NowPlayingItemId" = i."Id" - AND i."Type" = itemType - ORDER BY - t.unique_viewers DESC, t.latest_activity_date DESC; -END; -$BODY$; - -ALTER FUNCTION public.fs_most_popular_items(integer, text) - OWNER TO postgres; 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 deleted file mode 100644 index 459f42e..0000000 --- a/SQL Scripts/2. CREATE FUNCTIONS/CREATE FUNCTION fs_most_viewed_libraries.sql +++ /dev/null @@ -1,48 +0,0 @@ --- FUNCTION: public.fs_most_viewed_libraries(integer) - --- DROP FUNCTION IF EXISTS public.fs_most_viewed_libraries(integer); - -CREATE OR REPLACE FUNCTION public.fs_most_viewed_libraries( - days integer) - RETURNS TABLE("Plays" numeric, "Id" text, "Name" text, "ServerId" text, "IsFolder" boolean, "Type" text, "CollectionType" text, "ImageTagsPrimary" text) - LANGUAGE 'plpgsql' - COST 100 - VOLATILE PARALLEL UNSAFE - ROWS 1000 - -AS $BODY$ -BEGIN - RETURN QUERY - SELECT - sum(t."Plays"), - l."Id", - l."Name", - l."ServerId", - l."IsFolder", - l."Type", - l."CollectionType", - l."ImageTagsPrimary" - FROM ( - SELECT count(*) AS "Plays", - sum(jf_playback_activity."PlaybackDuration") AS "TotalPlaybackDuration", - jf_playback_activity."NowPlayingItemId" - FROM jf_playback_activity - WHERE - jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW() - - GROUP BY jf_playback_activity."NowPlayingItemId" - ORDER BY "Plays" DESC - ) t - JOIN jf_library_items i - ON i."Id" = t."NowPlayingItemId" - JOIN jf_libraries l - ON l."Id" = i."ParentId" - GROUP BY - l."Id" - ORDER BY - (sum( t."Plays")) DESC; -END; -$BODY$; - -ALTER FUNCTION public.fs_most_viewed_libraries(integer) - OWNER TO postgres; diff --git a/SQL Scripts/2. CREATE USER jfstat.sql b/SQL Scripts/2. CREATE USER jfstat.sql deleted file mode 100644 index cdabf5a..0000000 --- a/SQL Scripts/2. CREATE USER jfstat.sql +++ /dev/null @@ -1,11 +0,0 @@ --- Role: jfstat --- DROP ROLE IF EXISTS jfstat; - -CREATE ROLE jfstat WITH - LOGIN - SUPERUSER - INHERIT - CREATEDB - CREATEROLE - NOREPLICATION - ENCRYPTED PASSWORD 'SCRAM-SHA-256$4096:Cf1EY3ozsXG1sR/TWv/Xcw==$Om2f07jurCEEyaOGV/Fju9AGtUVj67Q1JFm0AZSiK4M=:lFaFNHdvtEHzC8l5qUf/uAWENJHa1T9jM3Bv5WDz66E='; \ 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 deleted file mode 100644 index 78f2357..0000000 --- a/SQL Scripts/3. CREATE VIEWS/CREATE VIEW jf_library_count_view.sql +++ /dev/null @@ -1,22 +0,0 @@ --- View: public.jf_library_count_view - --- DROP VIEW public.jf_library_count_view; - -CREATE OR REPLACE VIEW public.jf_library_count_view - AS - SELECT l."Id", - l."Name", - l."CollectionType", - count(DISTINCT i."Id") AS "Library_Count", - count(DISTINCT s."Id") AS "Season_Count", - count(DISTINCT e."Id") AS "Episode_Count" - FROM jf_libraries l - JOIN jf_library_items i ON i."ParentId" = l."Id" - LEFT JOIN jf_library_seasons s ON s."SeriesId" = i."Id" - LEFT JOIN jf_library_episodes e ON e."SeasonId" = s."Id" - GROUP BY l."Id", l."Name" - ORDER BY (count(DISTINCT i."Id")) DESC; - -ALTER TABLE public.jf_library_count_view - OWNER TO postgres; - 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 deleted file mode 100644 index 4d8239e..0000000 --- a/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_active_user.sql +++ /dev/null @@ -1,16 +0,0 @@ --- View: public.js_most_active_user - --- DROP VIEW public.js_most_active_user; - -CREATE OR REPLACE VIEW public.js_most_active_user - AS - SELECT count(*) AS "Plays", - jf_playback_activity."UserId", - jf_playback_activity."UserName" - FROM jf_playback_activity - GROUP BY jf_playback_activity."UserId", jf_playback_activity."UserName" - ORDER BY (count(*)) DESC; - -ALTER TABLE public.js_most_active_user - OWNER TO postgres; - 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 deleted file mode 100644 index 67ed567..0000000 --- a/SQL Scripts/3. CREATE VIEWS/CREATE VIEW js_most_used_clients.sql +++ /dev/null @@ -1,15 +0,0 @@ --- View: public.js_most_used_clients - --- DROP VIEW public.js_most_used_clients; - -CREATE OR REPLACE VIEW public.js_most_used_clients - AS - SELECT count(*) AS "Plays", - jf_playback_activity."Client" - FROM jf_playback_activity - GROUP BY jf_playback_activity."Client" - ORDER BY (count(*)) DESC; - -ALTER TABLE public.js_most_used_clients - OWNER TO postgres; - diff --git a/backend/api.js b/backend/api.js index 2f38550..b16b5e9 100644 --- a/backend/api.js +++ b/backend/api.js @@ -20,8 +20,17 @@ router.get("/getconfig", async (req, res) => { router.post("/setconfig", async (req, res) => { const { JF_HOST, JF_API_KEY } = req.body; + const { rows:getConfig } = await db.query('SELECT * FROM app_config where "ID"=1'); + + let query='UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1'; + if(getConfig.length===0) + { + query='INSERT INTO app_config ("JF_HOST","JF_API_KEY","APP_USER","APP_PASSWORD") VALUES ($1,$2,null,null)'; + } + + const { rows } = await db.query( - 'UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1', + query, [JF_HOST, JF_API_KEY] ); console.log({ JF_HOST: JF_HOST, JF_API_KEY: JF_API_KEY }); diff --git a/backend/models/jf_activity_watchdog.js b/backend/models/jf_activity_watchdog.js index 9eba789..e44c48b 100644 --- a/backend/models/jf_activity_watchdog.js +++ b/backend/models/jf_activity_watchdog.js @@ -13,6 +13,7 @@ const jf_activity_watchdog_columns = [ "SeasonId", "SeriesName", "PlaybackDuration", + "PlayMethod", "ActivityDateInserted", ]; @@ -32,6 +33,7 @@ const jf_activity_watchdog_columns = [ SeasonId: item.NowPlayingItem.SeasonId || null, SeriesName: item.NowPlayingItem.SeriesName || null, PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0, + PlayMethod:item.PlayState.PlayMethod, ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(), }); diff --git a/backend/models/jf_playback_activity.js b/backend/models/jf_playback_activity.js index a38ed9f..f396474 100644 --- a/backend/models/jf_playback_activity.js +++ b/backend/models/jf_playback_activity.js @@ -14,6 +14,7 @@ "SeasonId", "SeriesName", "PlaybackDuration", + "PlayMethod", "ActivityDateInserted", ]; @@ -33,6 +34,7 @@ SeasonId: item.NowPlayingItem.SeasonId || null, SeriesName: item.NowPlayingItem.SeriesName || null, PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0, + PlayMethod: item.PlayState.PlayMethod !== undefined ? item.PlayState.PlayMethod : item.PlayMethod , ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(), }); diff --git a/backend/models/jf_users.js b/backend/models/jf_users.js new file mode 100644 index 0000000..7eba662 --- /dev/null +++ b/backend/models/jf_users.js @@ -0,0 +1,23 @@ + ////////////////////////// pn delete move to playback + const jf_users_columns = [ + "Id", + "Name", + "PrimaryImageTag", + "LastLoginDate", + "LastActivityDate", + "IsAdministrator" + ]; + + const jf_users_mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + PrimaryImageTag: item.PrimaryImageTag, + LastLoginDate: item.LastLoginDate, + LastActivityDate: item.LastActivityDate, + IsAdministrator: item.Policy.IsAdministrator, + }); + + module.exports = { + jf_users_columns, + jf_users_mapping, + }; \ No newline at end of file diff --git a/backend/stats.js b/backend/stats.js index 62922c4..2da83e5 100644 --- a/backend/stats.js +++ b/backend/stats.js @@ -45,6 +45,20 @@ router.post("/getMostViewedMovies", async (req, res) => { }); +router.post("/getMostViewedMusic", async (req, res) => { + const {days} = req.body; + let _days=days; + if(days===undefined) + { + _days=30; + } + const { rows } = await db.query( + `select * from fs_most_played_items(${_days},'Audio') limit 5` + ); + res.send(rows); + +}); + router.post("/getMostViewedLibraries", async (req, res) => { @@ -61,17 +75,38 @@ router.post("/getMostViewedLibraries", async (req, res) => { }); -router.get("/getMostUsedClient", async (req, res) => { - const { rows } = await db.query('SELECT * FROM js_most_used_clients limit 5'); + + +router.post("/getMostUsedClient", async (req, res) => { + const {days} = req.body; + let _days=days; + if(days===undefined) + { + _days=30; + } + const { rows } = await db.query( + `select * from fs_most_used_clients(${_days}) limit 5` + ); res.send(rows); }); -router.get("/getMostActiveUsers", async (req, res) => { - const { rows } = await db.query('SELECT * FROM js_most_active_user limit 5'); + + +router.post("/getMostActiveUsers", async (req, res) => { + const {days} = req.body; + let _days=days; + if(days===undefined) + { + _days=30; + } + const { rows } = await db.query( + `select * from fs_most_active_user(${_days}) limit 5` + ); res.send(rows); }); + router.post("/getMostPopularMovies", async (req, res) => { const {days} = req.body; let _days=days; @@ -95,7 +130,7 @@ router.post("/getMostPopularSeries", async (req, res) => { { _days=30; } - console.log(`select * from fs_most_popular_items(${_days},'Series') limit 5`); + const { rows } = await db.query( `select * from fs_most_popular_items(${_days},'Series') limit 5` ); @@ -103,6 +138,21 @@ router.post("/getMostPopularSeries", async (req, res) => { }); +router.post("/getMostPopularMusic", async (req, res) => { + const {days} = req.body; + let _days=days; + if(days===undefined) + { + _days=30; + } + + const { rows } = await db.query( + `select * from fs_most_popular_items(${_days},'Audio') limit 5` + ); + res.send(rows); + +}); + router.get("/getPlaybackActivity", async (req, res) => { const { rows } = await db.query('SELECT * FROM jf_playback_activity'); @@ -110,4 +160,10 @@ router.get("/getPlaybackActivity", async (req, res) => { // console.log(`ENDPOINT CALLED: /getPlaybackActivity`); }); +router.get("/getAllUserActivity", async (req, res) => { + const { rows } = await db.query('SELECT * FROM jf_all_user_activity'); + res.send(rows); + // console.log(`ENDPOINT CALLED: /getPlaybackActivity`); +}); + module.exports = router; diff --git a/backend/sync.js b/backend/sync.js index 776e215..f33abeb 100644 --- a/backend/sync.js +++ b/backend/sync.js @@ -8,22 +8,12 @@ const sendMessageToClients = ws(8080); const router = express.Router(); -const { - jf_libraries_columns, - jf_libraries_mapping, -} = require("./models/jf_libraries"); -const { - jf_library_items_columns, - jf_library_items_mapping, -} = require("./models/jf_library_items"); -const { - jf_library_seasons_columns, - jf_library_seasons_mapping, -} = require("./models/jf_library_seasons"); -const { - jf_library_episodes_columns, - jf_library_episodes_mapping, -} = require("./models/jf_library_episodes"); +const {jf_libraries_columns,jf_libraries_mapping,} = require("./models/jf_libraries"); +const {jf_library_items_columns,jf_library_items_mapping,} = require("./models/jf_library_items"); +const {jf_library_seasons_columns,jf_library_seasons_mapping,} = require("./models/jf_library_seasons"); +const {jf_library_episodes_columns,jf_library_episodes_mapping,} = require("./models/jf_library_episodes"); + +const {jf_users_columns,jf_users_mapping,} = require("./models/jf_users"); /////////////////////////////////////////Functions class sync { @@ -32,6 +22,22 @@ class sync { this.apiKey = apiKey; } + async getUsers() { + try { + const url = `${this.hostUrl}/Users`; + console.log("getAdminUser: ", url); + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.apiKey, + }, + }); + return response.data; + } catch (error) { + console.log(error); + return []; + } + } + async getAdminUser() { try { const url = `${this.hostUrl}/Users`; @@ -51,10 +57,9 @@ class sync { } } - async getItem(itemID) { + async getItem(itemID,userid) { try { - const admins = await this.getAdminUser(); - const userid = admins[0].Id; + let url = `${this.hostUrl}/users/${userid}/Items`; if (itemID !== undefined) { url += `?ParentID=${itemID}`; @@ -68,7 +73,7 @@ class sync { const results = response.data.Items; if (itemID === undefined) { return results.filter((type) => - ["tvshows", "movies"].includes(type.CollectionType) + ["tvshows", "movies","music"].includes(type.CollectionType) ); } else { return results; @@ -78,11 +83,10 @@ class sync { return []; } } - async getSeasonsAndEpisodes(showId) { + async getSeasonsAndEpisodes(showId,userid) { const allSeasons = []; const allEpisodes = []; - - let seasonItems = await this.getItem(showId); + let seasonItems = await this.getItem(showId,userid); const seasonWithParent = seasonItems.map((items) => ({ ...items, ...{ ParentId: showId }, @@ -90,7 +94,7 @@ class sync { allSeasons.push(...seasonWithParent); for (let e = 0; e < seasonItems.length; e++) { const season = seasonItems[e]; - let episodeItems = await this.getItem(season.Id); + let episodeItems = await this.getItem(season.Id,userid); const episodeWithParent = episodeItems.map((items) => ({ ...items, ...{ ParentId: season.Id }, @@ -103,9 +107,8 @@ class sync { } ////////////////////////////////////////API Methods -///////////////////////////////////////writeLibraries -router.get("/writeLibraries", async (req, res) => { - let message = []; +///////////////////////////////////////Write Users +router.get("/writeUsers", async (req, res) => { const { rows } = await db.query('SELECT * FROM app_config where "ID"=1'); if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) { @@ -115,120 +118,113 @@ router.get("/writeLibraries", async (req, res) => { } const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); - const data = await _sync.getItem(); //getting all root folders aka libraries - // specify the columns to insert into + const data = await _sync.getUsers(); const existingIds = await db - .query('SELECT "Id" FROM jf_libraries') + .query('SELECT "Id" FROM jf_users') .then((res) => res.rows.map((row) => row.Id)); // get existing library Ids from the db - //data mapping - let dataToInsert = []; //filter fix if jf_libraries is empty if (existingIds.length === 0) { - // if there are no existing Ids in the table, map all items in the data array to the expected format - dataToInsert = await data.map(jf_libraries_mapping); + dataToInsert = await data.map(jf_users_mapping); } else { - // otherwise, filter only new data to insert dataToInsert = await data .filter((row) => !existingIds.includes(row.Id)) - .map(jf_libraries_mapping); + .map(jf_users_mapping); } - //Bulkinsert new data not on db if (dataToInsert.length !== 0) { - //insert new - await (async () => { - try { - await db.query("BEGIN"); - - const query = pgp.helpers.insert( - dataToInsert, - jf_libraries_columns, - "jf_libraries" - ); - await db.query(query); - - await db.query("COMMIT"); - message.push({ - Type: "Success", - Message: dataToInsert.length + " Rows Inserted.", - }); - sendMessageToClients(dataToInsert.length + " Rows Inserted."); - } catch (error) { - await db.query("ROLLBACK"); - message.push({ - Type: "Error", - Message: "Error performing bulk insert:" + error, - }); - sendMessageToClients({ - Message: "Error performing bulk insert:" + error, - }); - } - })(); - } else { - message.push({ Type: "Success", Message: "No new data to bulk insert" }); - sendMessageToClients({ Message: "No new data to bulk insert" }); + let result = await db.insertBulk("jf_users",dataToInsert,jf_users_columns); + if (result.Result === "SUCCESS") { + sendMessageToClients(dataToInsert.length + " Rows Inserted."); + } else { + sendMessageToClients({ + color: "red", + Message: "Error performing bulk insert:" + result.message, + }); + } } - //Bulk delete from db thats no longer on api - if (existingIds.length > data.length) { - await (async () => { - try { - await db.query("BEGIN"); - - const AllIds = data.map((row) => row.Id); - - const deleteQuery = { - text: `DELETE FROM jf_libraries WHERE "Id" NOT IN (${pgp.as.csv( - AllIds - )})`, - }; - const queries = [deleteQuery]; - for (let query of queries) { - await db.query(query); - } - - await db.query("COMMIT"); - - message.push({ - Type: "Success", - Message: existingIds.length - data.length + " Rows Removed.", - }); - sendMessageToClients( - existingIds.length - data.length + " Rows Removed." - ); - } catch (error) { - await db.query("ROLLBACK"); - - message.push({ - Type: "Error", - Message: "Error performing bulk removal:" + error, - }); - sendMessageToClients({ - Message: "Error performing bulk removal:" + error, - }); - } - })(); - } else { - message.push({ Type: "Success", Message: "No new data to bulk delete" }); - sendMessageToClients({ Message: "No new data to bulk delete" }); + + const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id )); + if (toDeleteIds.length > 0) { + let result = await db.deleteBulk("jf_users",toDeleteIds); + if (result.Result === "SUCCESS") { + sendMessageToClients(toDeleteIds.length + " Rows Removed."); + } else { + sendMessageToClients({color: "red",Message: result.message,}); + } + } - //Sent logs - res.send(message); + res.send(); +}); + +///////////////////////////////////////writeLibraries +router.get("/writeLibraries", async (req, res) => { + + const { rows } = await db.query('SELECT * FROM app_config where "ID"=1'); + if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) { + res.send({ error: "Config Details Not Found" }); + sendMessageToClients({ Message: "Error: Config details not found!" }); + return; + } + + const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); + const admins = await _sync.getAdminUser(); + const userid = admins[0].Id; + const data = await _sync.getItem(undefined,userid); //getting all root folders aka libraries + + const existingIds = await db + .query('SELECT "Id" FROM jf_libraries') + .then((res) => res.rows.map((row) => row.Id)); + + + let dataToInsert = []; + //filter fix if jf_libraries is empty + + if (existingIds.length === 0) { + dataToInsert = await data.map(jf_libraries_mapping); + } else { + dataToInsert = await data.filter((row) => !existingIds.includes(row.Id)).map(jf_libraries_mapping); + } + + if (dataToInsert.length !== 0) { + let result = await db.insertBulk("jf_libraries",dataToInsert,jf_libraries_columns); + if (result.Result === "SUCCESS") { + sendMessageToClients(dataToInsert.length + " Rows Inserted."); + } else { + sendMessageToClients({ + color: "red", + Message: "Error performing bulk insert:" + result.message, + }); + } + } + + const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id )); + if (toDeleteIds.length > 0) { + let result = await db.deleteBulk("jf_libraries",toDeleteIds); + if (result.Result === "SUCCESS") { + sendMessageToClients(toDeleteIds.length + " Rows Removed."); + } else { + sendMessageToClients({color: "red",Message: result.message,}); + } + + } + + res.send(); - console.log(`ENDPOINT CALLED: /writeLibraries: `); }); //////////////////////////////////////////////////////writeLibraryItems router.get("/writeLibraryItems", async (req, res) => { - let message = []; - const { rows: config } = await db.query( - 'SELECT * FROM app_config where "ID"=1' - ); + + const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1' ); + const { rows: cleanup } = await db.query('DELETE FROM jf_playback_activity where "NowPlayingItemId" not in (select "Id" from jf_library_items)' ); + sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" }); + if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) { res.send({ error: "Config Details Not Found" }); return; @@ -237,20 +233,18 @@ router.get("/writeLibraryItems", async (req, res) => { const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/2" }); - sendMessageToClients({ - color: "yellow", - Message: "Beginning Library Item Sync", - }); - //Get all Library items - //gets all libraries - const libraries = await _sync.getItem(); + sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",}); + + const admins = await _sync.getAdminUser(); + const userid = admins[0].Id; + const libraries = await _sync.getItem(undefined,userid); const data = []; let insertCounter = 0; let deleteCounter = 0; //for each item in library run get item using that id as the ParentId (This gets the children of the parent id) for (let i = 0; i < libraries.length; i++) { const item = libraries[i]; - let libraryItems = await _sync.getItem(item.Id); + let libraryItems = await _sync.getItem(item.Id,userid); const libraryItemsWithParent = libraryItems.map((items) => ({ ...items, ...{ ParentId: item.Id }, @@ -258,153 +252,79 @@ router.get("/writeLibraryItems", async (req, res) => { data.push(...libraryItemsWithParent); } - ///////////////////// const existingIds = await db .query('SELECT "Id" FROM jf_library_items') .then((res) => res.rows.map((row) => row.Id)); - //data mapping - let dataToInsert = []; //filter fix if jf_libraries is empty if (existingIds.length === 0) { - // if there are no existing Ids in the table, map all items in the data array to the expected format dataToInsert = await data.map(jf_library_items_mapping); } else { - // otherwise, filter only new data to insert dataToInsert = await data .filter((row) => !existingIds.includes(row.Id)) .map(jf_library_items_mapping); } - //Bulkinsert new data not on db + if (dataToInsert.length !== 0) { - //insert new - await (async () => { - try { - await db.query("BEGIN"); - - const query = pgp.helpers.insert( - dataToInsert, - jf_library_items_columns, - "jf_library_items" - ); - await db.query(query); - - await db.query("COMMIT"); - message.push({ - Type: "Success", - Message: dataToInsert.length + " Rows Inserted.", - }); - insertCounter += dataToInsert.length; - } catch (error) { - await db.query("ROLLBACK"); - message.push({ - Type: "Error", - Message: "Error performing bulk insert:" + error, - }); - sendMessageToClients({ - color: "red", - Message: "Error performing Item insert:" + error, - }); - } - })(); - } else { - message.push({ Type: "Success", Message: "No new data to bulk insert" }); + let result = await db.insertBulk("jf_library_items",dataToInsert,jf_library_items_columns); + if (result.Result === "SUCCESS") { + insertCounter += dataToInsert.length; + } else { + sendMessageToClients({ + color: "red", + Message: "Error performing bulk insert:" + result.message, + }); + } } - //Bulk delete from db thats no longer on api - if (existingIds.length > data.length) { - await (async () => { - try { - await db.query("BEGIN"); - - const AllIds = data.map((row) => row.Id); - - const deleteQuery = { - text: `DELETE FROM jf_library_items WHERE "Id" NOT IN (${pgp.as.csv( - AllIds - )})`, - }; - const queries = [deleteQuery]; - for (let query of queries) { - await db.query(query); - } - - await db.query("COMMIT"); - - message.push({ - Type: "Success", - Message: existingIds.length - data.length + " Rows Removed.", - }); - deleteCounter += existingIds.length - data.length; - } catch (error) { - await db.query("ROLLBACK"); - - message.push({ - Type: "Error", - Message: "Error performing bulk removal:" + error, - }); - sendMessageToClients({ - color: "red", - Message: "Error performing Item removal:" + error, - }); - } - })(); - } else { - message.push({ Type: "Success", Message: "No new data to bulk delete" }); - // sendMessageToClients({Message:"No new Library items to bulk delete"}); - } - //Sent logs - - sendMessageToClients({ - color: "dodgerblue", - Message: insertCounter + " Library Items Inserted.", - }); - sendMessageToClients({ - color: "orange", - Message: deleteCounter + " Library Items Removed.", - }); + + const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id )); + if (toDeleteIds.length > 0) { + let result = await db.deleteBulk("jf_library_items",toDeleteIds); + if (result.Result === "SUCCESS") { + deleteCounter +=toDeleteIds.length; + } else { + sendMessageToClients({color: "red",Message: result.message,}); + } + } + + sendMessageToClients({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",}); + sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",}); sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" }); - res.send(message); + res.send(); - console.log(`ENDPOINT CALLED: /writeLibraryItems: `); }); //////////////////////////////////////////////////////writeSeasonsAndEpisodes router.get("/writeSeasonsAndEpisodes", async (req, res) => { sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/2" }); - sendMessageToClients({ - color: "yellow", - Message: "Beginning Seasons and Episode sync", - }); - const message = []; - const { rows: config } = await db.query( - 'SELECT * FROM app_config where "ID"=1' - ); + sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",}); + + const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1'); + if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) { res.send({ error: "Config Details Not Found" }); return; } const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); - const { rows: shows } = await db.query( - `SELECT * FROM public.jf_library_items where "Type"='Series'` - ); + const { rows: shows } = await db.query(`SELECT * FROM public.jf_library_items where "Type"='Series'`); let insertSeasonsCount = 0; let insertEpisodeCount = 0; let deleteSeasonsCount = 0; let deleteEpisodeCount = 0; + + const admins = await _sync.getAdminUser(); + const userid = admins[0].Id; //loop for each show for (const show of shows) { - const data = await _sync.getSeasonsAndEpisodes(show.Id); + const data = await _sync.getSeasonsAndEpisodes(show.Id,userid); - // - //get existing seasons and episodes - console.log(show.Id); const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id)); let existingIdsEpisodes = []; @@ -448,202 +368,63 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => { if (seasonsToInsert.length !== 0) { let result = await db.insertBulk("jf_library_seasons",seasonsToInsert,jf_library_seasons_columns); if (result.Result === "SUCCESS") { - message.push({ - Type: "Success", - Message: seasonsToInsert.length + " Rows Inserted for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_seasons", - }); insertSeasonsCount += seasonsToInsert.length; } else { - message.push({ - Type: "Error", - Message: "Error performing bulk insert:" + result.message, - ItemId: show.Id, - TableName: "jf_library_seasons", - }); sendMessageToClients({ color: "red", Message: "Error performing bulk insert:" + result.message, }); } - } else { - message.push({ - Type: "Success", - Message: "No new data to bulk insert for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_seasons", - }); - } - + } const toDeleteIds = existingIdsSeasons.filter((id) =>!data.allSeasons.some((row) => row.Id === id )); //Bulk delete from db thats no longer on api if (toDeleteIds.length > 0) { - - - let table="jf_library_seasons"; - let result = await db.deleteBulk(table,toDeleteIds); + let result = await db.deleteBulk("jf_library_seasons",toDeleteIds); if (result.Result === "SUCCESS") { - message.push({ - Type: "Success", - Message: toDeleteIds.length + " Rows Removed for " + show.Name, - ItemId: show.Id, - TableName: table, - }); deleteSeasonsCount +=toDeleteIds.length; } else { - message.push({ - Type: "Error", - Message: result.message, - ItemId: show.Id, - TableName: table, - }); - sendMessageToClients({ - color: "red", - Message: result.message, - }); - - + sendMessageToClients({color: "red",Message: result.message,}); } - } else { - message.push({ - Type: "Success", - Message: "No new data to bulk delete for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_seasons", - }); - // sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); - } + } //insert delete episodes //Bulkinsert new data not on db if (episodesToInsert.length !== 0) { - //insert new - await (async () => { - try { - await db.query("BEGIN"); + let result = await db.insertBulk("jf_library_episodes",episodesToInsert,jf_library_episodes_columns); + if (result.Result === "SUCCESS") { + insertEpisodeCount += episodesToInsert.length; + } else { + sendMessageToClients({ + color: "red", + Message: "Error performing bulk insert:" + result.message, + }); + } + } - const query = pgp.helpers.insert( - episodesToInsert, - jf_library_episodes_columns, - "jf_library_episodes" - ); - await db.query(query); - - await db.query("COMMIT"); - message.push({ - Type: "Success", - Message: - episodesToInsert.length + " Rows Inserted for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_episodes", - }); - insertEpisodeCount += episodesToInsert.length; - // sendMessageToClients({color:'dodgerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name}); - } catch (error) { - await db.query("ROLLBACK"); - message.push({ - Type: "Error", - Message: "Error performing bulk insert:" + error, - ItemId: show.Id, - TableName: "jf_library_episodes", - }); - sendMessageToClients({ - color: "red", - Message: "Error performing bulk insert:" + error, - }); - } - })(); - } else { - message.push({ - Type: "Success", - Message: "No new data to bulk insert for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_episodes", - }); - // sendMessageToClients({Message:"No new data to bulk insert for " + show.Name}); - } + const toDeleteEpisodeIds = existingIdsEpisodes.filter((id) =>!data.allEpisodes.some((row) => (row.Id + row.ParentId) === id )); //Bulk delete from db thats no longer on api - if (existingIdsEpisodes.length > data.allEpisodes.length) { - await (async () => { - try { - await db.query("BEGIN"); + if (toDeleteEpisodeIds.length > 0) { + let result = await db.deleteBulk("jf_library_episodes",toDeleteEpisodeIds); + if (result.Result === "SUCCESS") { + deleteEpisodeCount +=toDeleteEpisodeIds.length; + } else { + sendMessageToClients({color: "red",Message: result.message,}); + } + + } - const AllIds = data.allEpisodes.map((row) => row.Id + row.ParentId); - - const deleteQuery = { - text: `DELETE FROM jf_library_episodes WHERE "Id" NOT IN (${pgp.as.csv( - AllIds - )})`, - }; - const queries = [deleteQuery]; - for (let query of queries) { - await db.query(query); - } - - await db.query("COMMIT"); - - message.push({ - Type: "Success", - Message: - existingIdsEpisodes.length - - data.allEpisodes.length + - " Rows Removed for " + - show.Name, - ItemId: show.Id, - TableName: "jf_library_episodes", - }); - deleteEpisodeCount += - existingIdsEpisodes.length - data.allEpisodes.length; - // sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name}); - } catch (error) { - await db.query("ROLLBACK"); - - message.push({ - Type: "Error", - Message: "Error performing bulk removal:" + error, - ItemId: show.Id, - TableName: "jf_library_episodes", - }); - sendMessageToClients({ - color: "red", - Message: "Error performing bulk removal:" + error, - }); - } - })(); - } else { - message.push({ - Type: "Success", - Message: "No new data to bulk delete for " + show.Name, - ItemId: show.Id, - TableName: "jf_library_episodes", - }); - // sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); - } + sendMessageToClients({ Message: "Sync complete for " + show.Name }); } - sendMessageToClients({ - color: "dodgerblue", - Message: insertSeasonsCount + " Seasons inserted.", - }); - sendMessageToClients({ - color: "orange", - Message: deleteSeasonsCount + " Seasons Removed.", - }); - sendMessageToClients({ - color: "dodgerblue", - Message: insertEpisodeCount + " Episodes inserted.", - }); - sendMessageToClients({ - color: "orange", - Message: deleteEpisodeCount + " Episodes Removed.", - }); + sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",}); + sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",}); + sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",}); + sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",}); sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" }); - res.send(message); + res.send(); - console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `); }); ////////////////////////////////////// diff --git a/backend/watchdog/ActivityMonitor.js b/backend/watchdog/ActivityMonitor.js index 959497a..90009f9 100644 --- a/backend/watchdog/ActivityMonitor.js +++ b/backend/watchdog/ActivityMonitor.js @@ -10,6 +10,10 @@ async function ActivityMonitor(interval) { const { rows: config } = await db.query( 'SELECT * FROM app_config where "ID"=1' ); + if(config.length===0) + { + return; + } const base_url = config[0].JF_HOST; const apiKey = config[0].JF_API_KEY; diff --git a/postgres-image/Dockerfile b/postgres-image/Dockerfile new file mode 100644 index 0000000..22fb88e --- /dev/null +++ b/postgres-image/Dockerfile @@ -0,0 +1,2 @@ +FROM postgres +COPY init.sql /docker-entrypoint-initdb.d/ \ No newline at end of file diff --git a/postgres-image/commands.txt b/postgres-image/commands.txt new file mode 100644 index 0000000..5ed1341 --- /dev/null +++ b/postgres-image/commands.txt @@ -0,0 +1,3 @@ +docker run --name jellystat-postgres -e POSTGRES_PASSWORD=fy1W0POt5$ -p 25432:5432 -d jellystat-postgres + +docker build -t jellystat-postgres . \ No newline at end of file diff --git a/postgres-image/init.sql b/postgres-image/init.sql new file mode 100644 index 0000000..7655502 --- /dev/null +++ b/postgres-image/init.sql @@ -0,0 +1,591 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 15.2 (Debian 15.2-1.pgdg110+1) +-- Dumped by pg_dump version 15.1 + +-- Started on 2023-03-21 19:12:22 UTC + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 3389 (class 1262 OID 16387) +-- Name: jfstat; Type: DATABASE; Schema: -; Owner: jfstat +-- + +CREATE DATABASE jfstat WITH TEMPLATE = template0 ENCODING = 'UTF8' LOCALE_PROVIDER = libc LOCALE = 'en_US.utf8'; + + +-- ALTER DATABASE jfstat OWNER TO jfstat; + +\connect jfstat + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- TOC entry 232 (class 1255 OID 41783) +-- Name: fs_most_active_user(integer); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_most_active_user(days integer) RETURNS TABLE("Plays" bigint, "UserId" text, "Name" text) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN QUERY + SELECT count(*) AS "Plays", + jf_playback_activity."UserId", + jf_playback_activity."UserName" AS "Name" + FROM jf_playback_activity + WHERE jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) AND NOW() + GROUP BY jf_playback_activity."UserId", jf_playback_activity."UserName" + ORDER BY (count(*)) DESC; +END; +$$; + + +ALTER FUNCTION public.fs_most_active_user(days integer) OWNER TO postgres; + +-- +-- TOC entry 246 (class 1255 OID 41695) +-- Name: fs_most_played_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_most_played_items(days integer, itemtype text) RETURNS TABLE("Plays" bigint, total_playback_duration numeric, "Name" text, "Id" text) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN QUERY + SELECT + t.plays, + t.total_playback_duration, + i."Name", + i."Id" + FROM ( + SELECT + count(*) AS plays, + sum(jf_playback_activity."PlaybackDuration") AS total_playback_duration, + jf_playback_activity."NowPlayingItemId" + FROM + jf_playback_activity + WHERE + jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW() + GROUP BY + jf_playback_activity."NowPlayingItemId" + ORDER BY + count(*) DESC + ) t + JOIN jf_library_items i + ON t."NowPlayingItemId" = i."Id" + AND i."Type" = itemType + ORDER BY + t.plays DESC; +END; +$$; + + +ALTER FUNCTION public.fs_most_played_items(days integer, itemtype text) OWNER TO postgres; + +-- +-- TOC entry 245 (class 1255 OID 41690) +-- Name: fs_most_popular_items(integer, text); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_most_popular_items(days integer, itemtype text) RETURNS TABLE(unique_viewers bigint, latest_activity_date timestamp with time zone, "Name" text, "Id" text) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN QUERY + SELECT + t.unique_viewers, + t.latest_activity_date, + i."Name", + i."Id" + FROM ( + SELECT + jf_playback_activity."NowPlayingItemId", + count(DISTINCT jf_playback_activity."UserId") AS unique_viewers, + latest_activity_date.latest_date AS latest_activity_date + FROM + jf_playback_activity + JOIN ( + SELECT + jf_playback_activity_1."NowPlayingItemId", + max(jf_playback_activity_1."ActivityDateInserted") AS latest_date + FROM + jf_playback_activity jf_playback_activity_1 + GROUP BY jf_playback_activity_1."NowPlayingItemId" + ) latest_activity_date + ON jf_playback_activity."NowPlayingItemId" = latest_activity_date."NowPlayingItemId" + WHERE + jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW() + GROUP BY + jf_playback_activity."NowPlayingItemId", latest_activity_date.latest_date + ) t + JOIN jf_library_items i + ON t."NowPlayingItemId" = i."Id" + AND i."Type" = itemType + ORDER BY + t.unique_viewers DESC, t.latest_activity_date DESC; +END; +$$; + + +ALTER FUNCTION public.fs_most_popular_items(days integer, itemtype text) OWNER TO postgres; + +-- +-- TOC entry 231 (class 1255 OID 41730) +-- Name: fs_most_used_clients(integer); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_most_used_clients(days integer) RETURNS TABLE("Plays" bigint, "Client" text) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN QUERY + SELECT count(*) AS "Plays", + jf_playback_activity."Client" + FROM jf_playback_activity + WHERE jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) AND NOW() + GROUP BY jf_playback_activity."Client" + ORDER BY (count(*)) DESC; +END; +$$; + + +ALTER FUNCTION public.fs_most_used_clients(days integer) OWNER TO postgres; + +-- +-- TOC entry 244 (class 1255 OID 41701) +-- Name: fs_most_viewed_libraries(integer); Type: FUNCTION; Schema: public; Owner: postgres +-- + +CREATE FUNCTION public.fs_most_viewed_libraries(days integer) RETURNS TABLE("Plays" numeric, "Id" text, "Name" text, "ServerId" text, "IsFolder" boolean, "Type" text, "CollectionType" text, "ImageTagsPrimary" text) + LANGUAGE plpgsql + AS $$ +BEGIN + RETURN QUERY + SELECT + sum(t."Plays"), + l."Id", + l."Name", + l."ServerId", + l."IsFolder", + l."Type", + l."CollectionType", + l."ImageTagsPrimary" + FROM ( + SELECT count(*) AS "Plays", + sum(jf_playback_activity."PlaybackDuration") AS "TotalPlaybackDuration", + jf_playback_activity."NowPlayingItemId" + FROM jf_playback_activity + WHERE + jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW() + + GROUP BY jf_playback_activity."NowPlayingItemId" + ORDER BY "Plays" DESC + ) t + JOIN jf_library_items i + ON i."Id" = t."NowPlayingItemId" + JOIN jf_libraries l + ON l."Id" = i."ParentId" + GROUP BY + l."Id" + ORDER BY + (sum( t."Plays")) DESC; +END; +$$; + + +ALTER FUNCTION public.fs_most_viewed_libraries(days integer) OWNER TO postgres; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- TOC entry 220 (class 1259 OID 16395) +-- Name: app_config; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.app_config ( + "ID" integer NOT NULL, + "JF_HOST" text, + "JF_API_KEY" text, + "APP_USER" text, + "APP_PASSWORD" text +); + + +ALTER TABLE public.app_config OWNER TO postgres; + +-- +-- TOC entry 221 (class 1259 OID 16402) +-- Name: app_config_ID_seq; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +ALTER TABLE public.app_config ALTER COLUMN "ID" ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public."app_config_ID_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- TOC entry 228 (class 1259 OID 41300) +-- Name: jf_activity_watchdog; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_activity_watchdog ( + "Id" text NOT NULL, + "IsPaused" boolean DEFAULT false, + "UserId" text, + "UserName" text, + "Client" text, + "DeviceName" text, + "DeviceId" text, + "ApplicationVersion" text, + "NowPlayingItemId" text, + "NowPlayingItemName" text, + "SeasonId" text, + "SeriesName" text, + "EpisodeId" text, + "PlaybackDuration" bigint, + "ActivityDateInserted" timestamp with time zone, + "PlayMethod" text +); + + +ALTER TABLE public.jf_activity_watchdog OWNER TO postgres; + +-- +-- TOC entry 227 (class 1259 OID 41294) +-- Name: jf_playback_activity; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_playback_activity ( + "Id" text NOT NULL, + "IsPaused" boolean DEFAULT false, + "UserId" text, + "UserName" text, + "Client" text, + "DeviceName" text, + "DeviceId" text, + "ApplicationVersion" text, + "NowPlayingItemId" text, + "NowPlayingItemName" text, + "SeasonId" text, + "SeriesName" text, + "EpisodeId" text, + "PlaybackDuration" bigint, + "ActivityDateInserted" timestamp with time zone, + "PlayMethod" text +); + + +ALTER TABLE public.jf_playback_activity OWNER TO postgres; + +-- +-- TOC entry 229 (class 1259 OID 41731) +-- Name: jf_users; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_users ( + "Id" text NOT NULL, + "Name" text, + "PrimaryImageTag" text, + "LastLoginDate" timestamp with time zone, + "LastActivityDate" timestamp with time zone, + "IsAdministrator" boolean +); + + +ALTER TABLE public.jf_users OWNER TO postgres; + +-- +-- TOC entry 230 (class 1259 OID 41771) +-- Name: jf_all_user_activity; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW public.jf_all_user_activity AS + SELECT u."Id" AS "UserId", + u."PrimaryImageTag", + u."Name" AS "UserName", + CASE + WHEN (j."SeriesName" IS NULL) THEN j."NowPlayingItemName" + ELSE ((j."SeriesName" || ' - '::text) || j."NowPlayingItemName") + END AS "LastWatched", + j."ActivityDateInserted" AS "LastActivityDate", + ((j."Client" || ' - '::text) || j."DeviceName") AS "LastClient", + plays."TotalPlays", + plays."TotalWatchTime", + (now() - j."ActivityDateInserted") AS "LastSeen" + FROM ((( SELECT jf_users."Id", + jf_users."Name", + jf_users."PrimaryImageTag", + jf_users."LastLoginDate", + jf_users."LastActivityDate", + jf_users."IsAdministrator" + FROM public.jf_users) u + LEFT JOIN LATERAL ( SELECT jf_playback_activity."Id", + jf_playback_activity."IsPaused", + jf_playback_activity."UserId", + jf_playback_activity."UserName", + jf_playback_activity."Client", + jf_playback_activity."DeviceName", + jf_playback_activity."DeviceId", + jf_playback_activity."ApplicationVersion", + jf_playback_activity."NowPlayingItemId", + jf_playback_activity."NowPlayingItemName", + jf_playback_activity."SeasonId", + jf_playback_activity."SeriesName", + jf_playback_activity."EpisodeId", + jf_playback_activity."PlaybackDuration", + jf_playback_activity."ActivityDateInserted" + FROM public.jf_playback_activity + WHERE (jf_playback_activity."UserId" = u."Id") + ORDER BY jf_playback_activity."ActivityDateInserted" DESC + LIMIT 1) j ON (true)) + LEFT JOIN LATERAL ( SELECT count(*) AS "TotalPlays", + sum(jf_playback_activity."PlaybackDuration") AS "TotalWatchTime" + FROM public.jf_playback_activity + WHERE (jf_playback_activity."UserId" = u."Id")) plays ON (true)) + ORDER BY j."ActivityDateInserted"; + + +ALTER TABLE public.jf_all_user_activity OWNER TO postgres; + +-- +-- TOC entry 222 (class 1259 OID 16411) +-- Name: jf_libraries; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_libraries ( + "Id" text NOT NULL, + "Name" text NOT NULL, + "ServerId" text, + "IsFolder" boolean DEFAULT true NOT NULL, + "Type" text DEFAULT 'CollectionFolder'::text NOT NULL, + "CollectionType" text NOT NULL, + "ImageTagsPrimary" text +); + + +ALTER TABLE public.jf_libraries OWNER TO postgres; + +-- +-- TOC entry 226 (class 1259 OID 25160) +-- Name: jf_library_count_view; Type: VIEW; Schema: public; Owner: postgres +-- + +CREATE VIEW public.jf_library_count_view AS +SELECT + NULL::text AS "Id", + NULL::text AS "Name", + NULL::text AS "CollectionType", + NULL::bigint AS "Library_Count", + NULL::bigint AS "Season_Count", + NULL::bigint AS "Episode_Count"; + + +ALTER TABLE public.jf_library_count_view OWNER TO postgres; + +-- +-- TOC entry 225 (class 1259 OID 24906) +-- Name: jf_library_episodes; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_library_episodes ( + "Id" text NOT NULL, + "EpisodeId" text NOT NULL, + "Name" text, + "ServerId" text, + "PremiereDate" timestamp with time zone, + "OfficialRating" text, + "CommunityRating" double precision, + "RunTimeTicks" bigint, + "ProductionYear" integer, + "IndexNumber" integer, + "ParentIndexNumber" integer, + "Type" text, + "ParentLogoItemId" text, + "ParentBackdropItemId" text, + "ParentBackdropImageTags" text, + "SeriesId" text, + "SeasonId" text, + "SeasonName" text, + "SeriesName" text +); + + +ALTER TABLE public.jf_library_episodes OWNER TO postgres; + +-- +-- TOC entry 223 (class 1259 OID 24599) +-- Name: jf_library_items; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_library_items ( + "Id" text NOT NULL, + "Name" text NOT NULL, + "ServerId" text, + "PremiereDate" timestamp with time zone, + "EndDate" timestamp with time zone, + "CommunityRating" double precision, + "RunTimeTicks" bigint, + "ProductionYear" integer, + "IsFolder" boolean, + "Type" text, + "Status" text, + "ImageTagsPrimary" text, + "ImageTagsBanner" text, + "ImageTagsLogo" text, + "ImageTagsThumb" text, + "BackdropImageTags" text, + "ParentId" text NOT NULL +); + + +ALTER TABLE public.jf_library_items OWNER TO postgres; + +-- +-- TOC entry 224 (class 1259 OID 24731) +-- Name: jf_library_seasons; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.jf_library_seasons ( + "Id" text NOT NULL, + "Name" text, + "ServerId" text, + "IndexNumber" integer, + "Type" text, + "ParentLogoItemId" text, + "ParentBackdropItemId" text, + "ParentBackdropImageTags" text, + "SeriesName" text, + "SeriesId" text, + "SeriesPrimaryImageTag" text +); + + +ALTER TABLE public.jf_library_seasons OWNER TO postgres; + +-- +-- TOC entry 3228 (class 2606 OID 16401) +-- Name: app_config app_config_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.app_config + ADD CONSTRAINT app_config_pkey PRIMARY KEY ("ID"); + + +-- +-- TOC entry 3230 (class 2606 OID 16419) +-- Name: jf_libraries jf_libraries_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.jf_libraries + ADD CONSTRAINT jf_libraries_pkey PRIMARY KEY ("Id"); + + +-- +-- TOC entry 3236 (class 2606 OID 24912) +-- Name: jf_library_episodes jf_library_episodes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.jf_library_episodes + ADD CONSTRAINT jf_library_episodes_pkey PRIMARY KEY ("Id"); + + +-- +-- TOC entry 3232 (class 2606 OID 24605) +-- Name: jf_library_items jf_library_items_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.jf_library_items + ADD CONSTRAINT jf_library_items_pkey PRIMARY KEY ("Id"); + + +-- +-- TOC entry 3234 (class 2606 OID 24737) +-- Name: jf_library_seasons jf_library_seasons_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.jf_library_seasons + ADD CONSTRAINT jf_library_seasons_pkey PRIMARY KEY ("Id"); + + +-- +-- TOC entry 3238 (class 2606 OID 41737) +-- Name: jf_users jf_users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.jf_users + ADD CONSTRAINT jf_users_pkey PRIMARY KEY ("Id"); + + +-- +-- TOC entry 3382 (class 2618 OID 25163) +-- Name: jf_library_count_view _RETURN; Type: RULE; Schema: public; Owner: postgres +-- + +CREATE OR REPLACE VIEW public.jf_library_count_view AS + SELECT l."Id", + l."Name", + l."CollectionType", + count(DISTINCT i."Id") AS "Library_Count", + count(DISTINCT s."Id") AS "Season_Count", + count(DISTINCT e."Id") AS "Episode_Count" + FROM (((public.jf_libraries l + JOIN public.jf_library_items i ON ((i."ParentId" = l."Id"))) + LEFT JOIN public.jf_library_seasons s ON ((s."SeriesId" = i."Id"))) + LEFT JOIN public.jf_library_episodes e ON ((e."SeasonId" = s."Id"))) + GROUP BY l."Id", l."Name" + ORDER BY (count(DISTINCT i."Id")) DESC; + + +-- +-- TOC entry 3239 (class 2606 OID 24617) +-- Name: jf_library_items jf_library_items_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.jf_library_items + ADD CONSTRAINT jf_library_items_fkey FOREIGN KEY ("ParentId") REFERENCES public.jf_libraries("Id") ON DELETE SET NULL NOT VALID; + + +-- +-- TOC entry 3390 (class 0 OID 0) +-- Dependencies: 3239 +-- Name: CONSTRAINT jf_library_items_fkey ON jf_library_items; Type: COMMENT; Schema: public; Owner: postgres +-- + +COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items IS 'jf_library'; + + +-- Completed on 2023-03-21 19:12:22 UTC + +-- +-- PostgreSQL database dump complete +-- + diff --git a/public/index.html b/public/index.html index aa069f2..1f71f81 100644 --- a/public/index.html +++ b/public/index.html @@ -24,7 +24,7 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + JellyStat diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..5fc11b2 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "JellyStat", + "name": "Statistics for Jellyfin", "icons": [ { "src": "favicon.ico", diff --git a/src/App.css b/src/App.css index 4839435..ada2f63 100644 --- a/src/App.css +++ b/src/App.css @@ -1,11 +1,7 @@ -.App { - /* text-align: center; */ - /* padding-bottom: 5%; */ - display: grid; - gap: 1rem; - /* grid-template-areas: - "sidenav main"; */ - grid-template-columns: auto 1fr; + + +main{ + padding-inline: 10px; } .App-logo { diff --git a/src/App.js b/src/App.js index 87d09dd..e3aa058 100644 --- a/src/App.js +++ b/src/App.js @@ -13,21 +13,23 @@ import Loading from './pages/components/loading'; import Setup from './pages/setup'; -import SideNav from './pages/components/sidenav'; +import Navbar from './pages/components/navbar'; import Home from './pages/home'; import Settings from './pages/settings'; -import Activity from './pages/activity'; -import UserActivity from './pages/useractivity'; +import Users from './pages/users'; +import UserInfo from './pages/user-info'; import Libraries from './pages/libraries'; +import ErrorPage from './pages/components/error'; import RecentlyPlayed from './pages/components/recentlyplayed'; -import UserData from './pages/userdata'; +import Testing from './pages/testing'; function App() { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); + const [errorFlag, seterrorFlag] = useState(false); useEffect(() => { @@ -37,8 +39,10 @@ function App() { const newConfig = await Config(); if(newConfig !== 'ERR_NETWORK'){ setConfig(newConfig); - setLoading(false); + }else{ + seterrorFlag(true); } + setLoading(false); } catch (error) { @@ -55,6 +59,10 @@ if (loading) { return ; } +if (errorFlag) { + return ; +} + if (!config || config.apiKey ==null) { return ; } @@ -63,16 +71,16 @@ if (!config || config.apiKey ==null) { return (
- +
} /> } /> - } /> + } /> + } /> } /> - } /> - } /> + } /> } />
diff --git a/src/index.css b/src/index.css index 3f17a05..fa90343 100644 --- a/src/index.css +++ b/src/index.css @@ -17,3 +17,25 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + + +html { + overflow: auto; /* show scrollbar when needed */ +} + +html::-webkit-scrollbar { + width: 10px; /* set scrollbar width */ +} + +html::-webkit-scrollbar-track { + background-color: transparent; /* set track color */ +} + +html::-webkit-scrollbar-thumb { + background-color: #8888884d; /* set thumb color */ + border-radius: 5px; /* round corners */ +} + +html::-webkit-scrollbar-thumb:hover { + background-color: #88888883; /* set thumb color */ +} \ No newline at end of file diff --git a/src/lib/navdata.js b/src/lib/navdata.js index a34330c..251a59a 100644 --- a/src/lib/navdata.js +++ b/src/lib/navdata.js @@ -18,36 +18,24 @@ export const navData = [ }, { id: 1, - icon: , - text: "Activity", - link: "activity" + icon: , + text: "Users", + link: "users" }, { id: 2, icon: , text: "Libraries", link: "libraries" - } , - { - id: 3, - icon: , - text: "Recently Played", - link: "recent" }, { id: 4, - icon: , - text: "User Activity", - link: "usersactivity" + icon: , + text: "Component Testing Playground", + link: "testing" }, { id: 5, - icon: , - text: "Library Data", - link: "userdata" - }, - { - id: 6, icon: , text: "Settings", link: "settings" diff --git a/src/pages/components/StatsCards.js b/src/pages/components/WatchStatistics.js similarity index 88% rename from src/pages/components/StatsCards.js rename to src/pages/components/WatchStatistics.js index d3d7a3d..d00eab2 100644 --- a/src/pages/components/StatsCards.js +++ b/src/pages/components/WatchStatistics.js @@ -7,9 +7,12 @@ import MostUsedClient from "./statCards/most_used_client"; import MostActiveUsers from "./statCards/most_active_users"; import MPSeries from "./statCards/mp_series"; import MPMovies from "./statCards/mp_movies"; +import MVMusic from "./statCards/mv_music"; +import MPMusic from "./statCards/mp_music"; + import "../css/statCard.css"; -function StatCards() { +function WatchStatistics() { const [days, setDays] = useState(30); const [input, setInput] = useState(30); @@ -50,12 +53,15 @@ function StatCards() { + + +
); } -export default StatCards; +export default WatchStatistics; diff --git a/src/pages/components/error.js b/src/pages/components/error.js new file mode 100644 index 0000000..7348b87 --- /dev/null +++ b/src/pages/components/error.js @@ -0,0 +1,12 @@ +import React from "react"; +import "../css/error.css"; + +function ErrorPage(props) { + return ( +
+
{props.message}
+
+ ); +} + +export default ErrorPage; \ No newline at end of file diff --git a/src/pages/components/libraryOverview.js b/src/pages/components/libraryOverview.js index 12754fe..7bfdc39 100644 --- a/src/pages/components/libraryOverview.js +++ b/src/pages/components/libraryOverview.js @@ -1,27 +1,21 @@ import "../css/libraryOverview.css"; -import Config from "../../lib/config"; import React, { useState, useEffect } from "react"; import axios from "axios"; import Loading from "./loading"; +import LibraryStatComponent from "./libraryStatCard/library-stat-component"; + import TvLineIcon from "remixicon-react/TvLineIcon"; import FilmLineIcon from "remixicon-react/FilmLineIcon"; export default function LibraryOverView() { + const SeriesIcon= ; + const MovieIcon= ; const [data, setData] = useState([]); - const [base_url, setURL] = useState(""); + useEffect(() => { - if (base_url === "") { - Config() - .then((config) => { - setURL(config.hostUrl); - }) - .catch((error) => { - console.log(error); - }); - } const fetchData = () => { const url = `/stats/getLibraryOverview`; axios @@ -33,7 +27,7 @@ export default function LibraryOverView() { if (!data || data.length === 0) { fetchData(); } - }, [data, base_url]); + }, [data]); if (data.length === 0) { return ; @@ -43,67 +37,12 @@ export default function LibraryOverView() {

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}

-
- ))} -
-
-
- -
+ stat.CollectionType === "movies")} heading={"MOVIE LIBRARIES"} units={"MOVIES"} icon={MovieIcon}/> + stat.CollectionType === "tvshows")} heading={"SHOW LIBRARIES"} units={"SERIES / SEASONS / EPISODES"} icon={SeriesIcon}/> + stat.CollectionType === "music")} heading={"MUSIC LIBRARIES"} units={"SONGS"} icon={SeriesIcon}/>
+ ); } diff --git a/src/pages/components/libraryStatCard/library-stat-component.js b/src/pages/components/libraryStatCard/library-stat-component.js new file mode 100644 index 0000000..6bb3d71 --- /dev/null +++ b/src/pages/components/libraryStatCard/library-stat-component.js @@ -0,0 +1,40 @@ +import React from "react"; + +function LibraryStatComponent(props) { + + if (props.data.length === 0) { + return <>; + } + + return ( +
+ +
+
+ {props.icon} +
+
+ +
+
+
{props.heading}
+
{props.units}
+
+ +
+ {props.data + .map((item, index) => ( +
+

{index + 1}

+

{item.Name}

+

{item.CollectionType =='tvshows'? (item.Library_Count+' / '+item.Season_Count+' / '+item.Episode_Count): item.Library_Count}

+
+ ))} +
+
+ +
+ ); +} + +export default LibraryStatComponent; diff --git a/src/pages/components/navbar.js b/src/pages/components/navbar.js new file mode 100644 index 0000000..4a17c22 --- /dev/null +++ b/src/pages/components/navbar.js @@ -0,0 +1,19 @@ + +import { NavLink } from "react-router-dom"; +import { navData } from "../../lib/navdata"; +import "../css/navbar.css" + + + +export default function Navbar() { + return ( +
+ {navData.map(item =>{ + return + {item.icon} + {item.text} + + })} +
+ ) +} \ No newline at end of file diff --git a/src/pages/components/sessions.js b/src/pages/components/sessions.js index 94945ef..a6aab5c 100644 --- a/src/pages/components/sessions.js +++ b/src/pages/components/sessions.js @@ -6,7 +6,7 @@ import API from "../../classes/jellyfin-api"; import "../css/sessions.css"; // import "../../App.css" -import SessionCard from "./session-card"; +import SessionCard from "./sessions/session-card"; import Loading from "./loading"; @@ -44,7 +44,8 @@ function Sessions() { } if (data.length === 0) { - return(
+ return( +

Sessions

No Active Sessions Found @@ -53,9 +54,9 @@ function Sessions() { } return ( -
+

Sessions

-
+
{data && data .sort((a, b) => diff --git a/src/pages/components/session-card.js b/src/pages/components/sessions/session-card.js similarity index 100% rename from src/pages/components/session-card.js rename to src/pages/components/sessions/session-card.js diff --git a/src/pages/components/settings/WebSocketComponent .js b/src/pages/components/settings/TerminalComponent.js similarity index 95% rename from src/pages/components/settings/WebSocketComponent .js rename to src/pages/components/settings/TerminalComponent.js index 0ff3e35..d13a1ed 100644 --- a/src/pages/components/settings/WebSocketComponent .js +++ b/src/pages/components/settings/TerminalComponent.js @@ -1,7 +1,7 @@ import React, { useEffect, useState, useRef } from 'react'; import '../../css/websocket/websocket.css'; -const WebSocketComponent = () => { +const TerminalComponent = () => { const [messages, setMessages] = useState([]); const messagesEndRef = useRef(null); @@ -46,4 +46,4 @@ const WebSocketComponent = () => { ); }; -export default WebSocketComponent; +export default TerminalComponent; diff --git a/src/pages/components/settings/librarySync.js b/src/pages/components/settings/librarySync.js index 7e3bc47..5715d80 100644 --- a/src/pages/components/settings/librarySync.js +++ b/src/pages/components/settings/librarySync.js @@ -1,17 +1,27 @@ import React, { useState } from "react"; import axios from "axios"; -// import Config from "../../../lib/config"; -// import Loading from "../loading"; + import "../../css/settings.css"; export default function LibrarySync() { const [processing, setProcessing] = useState(false); - async function writeSeasonsAndEpisodes() { + async function beginSync() { setProcessing(true); + await axios + .get("/sync/writeLibraries") + .then((response) => { + if (response.status === 200) { + // isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); + await axios .get("/sync/writeLibraryItems") .then((response) => { @@ -34,13 +44,25 @@ export default function LibrarySync() { .catch((error) => { console.log(error); }); + + + await axios + .get("/sync/writeUsers") + .then((response) => { + if (response.status === 200) { + // isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); setProcessing(false); // return { isValid: isValid, errorMessage: errorMessage }; } const handleClick = () => { - writeSeasonsAndEpisodes(); + beginSync(); console.log('Button clicked!'); } diff --git a/src/pages/components/sidenav.js b/src/pages/components/sidenav.js deleted file mode 100644 index 079547e..0000000 --- a/src/pages/components/sidenav.js +++ /dev/null @@ -1,28 +0,0 @@ -import "../css/sidenav.css" -import { NavLink } from "react-router-dom"; - -import MenuUnfoldFillIcon from 'remixicon-react/MenuUnfoldFillIcon'; -import MenuFoldFillIcon from 'remixicon-react/MenuFoldFillIcon'; - -import { navData } from "../../lib/navdata"; -import { useState } from "react"; - -export default function Sidenav() { - const [open, setopen] = useState(false) - const toggleOpen = () => { - setopen(!open) - } - return ( -
- - {navData.map(item =>{ - return - {item.icon} - {open? item.text : ''} - - })} -
- ) -} \ No newline at end of file diff --git a/src/pages/components/statCards/most_active_users.js b/src/pages/components/statCards/most_active_users.js index 3a5b600..78d9502 100644 --- a/src/pages/components/statCards/most_active_users.js +++ b/src/pages/components/statCards/most_active_users.js @@ -1,14 +1,16 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; import Config from "../../../lib/config"; +import StatComponent from "./statsComponent"; import ComponentLoading from "../ComponentLoading"; import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon"; -function MostActiveUsers() { +function MostActiveUsers(props) { const [data, setData] = useState([]); + const [days, setDays] = useState(30); const [imgError, setImgError] = useState(false); const [config, setConfig] = useState(null); @@ -31,7 +33,11 @@ function MostActiveUsers() { const url = `/stats/getMostActiveUsers`; axios - .get(url) + .post(url, {days:props.days}, { + headers: { + "Content-Type": "application/json", + }, + }) .then((data) => { setData(data.data); }) @@ -50,9 +56,15 @@ function MostActiveUsers() { fetchLibraries(); } + if (days !== props.days) { + setDays(props.days); + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); return () => clearInterval(intervalId); - }, [data, config]); + }, [data, config, days,props.days]); @@ -95,30 +107,7 @@ function MostActiveUsers() { > }
-
-
- -
MOST ACTIVE USERS
-
Plays
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.UserName}

-

{item.Plays}

-
- - ))} - -
-
- +
); diff --git a/src/pages/components/statCards/most_used_client.js b/src/pages/components/statCards/most_used_client.js index 863efc5..8721e54 100644 --- a/src/pages/components/statCards/most_used_client.js +++ b/src/pages/components/statCards/most_used_client.js @@ -1,58 +1,49 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; -import Config from "../../../lib/config"; + +import StatComponent from "./statsComponent"; import ComponentLoading from "../ComponentLoading"; - import ComputerLineIcon from "remixicon-react/ComputerLineIcon"; -function MostUsedClient() { +function MostUsedClient(props) { const [data, setData] = useState([]); -// const [base_url, setURL] = useState(""); - - const [config, setConfig] = useState(null); + const [days, setDays] = useState(30); useEffect(() => { - const fetchConfig = async () => { - try { - const newConfig = await Config(); - setConfig(newConfig); - } catch (error) { - if (error.code === "ERR_NETWORK") { - console.log(error); - } - } - }; const fetchLibraries = () => { - if (config) { const url = `/stats/getMostUsedClient`; axios - .get(url) + .post(url, {days:props.days}, { + headers: { + "Content-Type": "application/json", + }, + }) .then((data) => { setData(data.data); }) .catch((error) => { console.log(error); }); - } }; - if (!config) { - fetchConfig(); - } - if (!data || data.length===0) { fetchLibraries(); } + if (days !== props.days) { + setDays(props.days); + fetchLibraries(); + } + const intervalId = setInterval(fetchLibraries, 60000 * 5); return () => clearInterval(intervalId); - }, [data, config]); + }, [data, days,props.days]); if (!data) { return( @@ -75,30 +66,7 @@ function MostUsedClient() {
-
-
- -
MOST USED CLIENTS
-
Plays
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.Client}

-

{item.Plays}

-
- - ))} - -
-
- +
); diff --git a/src/pages/components/statCards/mp_movies.js b/src/pages/components/statCards/mp_movies.js index 590f129..ee2531c 100644 --- a/src/pages/components/statCards/mp_movies.js +++ b/src/pages/components/statCards/mp_movies.js @@ -4,7 +4,7 @@ import Config from "../../../lib/config"; import ComponentLoading from "../ComponentLoading"; -// import PlaybackActivity from "./components/playbackactivity"; +import StatComponent from "./statsComponent"; function MPMovies(props) { const [data, setData] = useState([]); @@ -98,30 +98,7 @@ function MPMovies(props) { >
-
-
- -
MOST POPULAR MOVIES
-
Users
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.Name}

-

{item.unique_viewers}

-
- - ))} - -
-
- +
); diff --git a/src/pages/components/statCards/mp_music.js b/src/pages/components/statCards/mp_music.js new file mode 100644 index 0000000..c2e7cfe --- /dev/null +++ b/src/pages/components/statCards/mp_music.js @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ComponentLoading from "../ComponentLoading"; +import StatComponent from "./statsComponent"; + +// import PlaybackActivity from "./components/playbackactivity"; + +function MPMusic(props) { + const [data, setData] = useState([]); + const [days, setDays] = useState(30); +// const [base_url, setURL] = useState(""); + + const [config, setConfig] = useState(null); + + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config(); + setConfig(newConfig); + } catch (error) { + if (error.code === "ERR_NETWORK") { + console.log(error); + } + } + }; + + const fetchLibraries = () => { + if (config) { + const url = `/stats/getMostPopularMusic`; + + axios + .post(url, { days: props.days }, { + headers: { + "Content-Type": "application/json", + }, + }) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + if (!config) { + fetchConfig(); + } + + if (!data || data.length === 0) { + fetchLibraries(); + } + + if (days !== props.days) { + setDays(props.days); + fetchLibraries(); + } + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, config, days,props.days]); + + if (!data) { + return( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + + return ( +
+ +
+ + +
+ + + +
+ ); +} + +export default MPMusic; diff --git a/src/pages/components/statCards/mp_series.js b/src/pages/components/statCards/mp_series.js index 816d796..425b563 100644 --- a/src/pages/components/statCards/mp_series.js +++ b/src/pages/components/statCards/mp_series.js @@ -3,6 +3,7 @@ import axios from "axios"; import Config from "../../../lib/config"; import ComponentLoading from "../ComponentLoading"; +import StatComponent from "./statsComponent"; // import PlaybackActivity from "./components/playbackactivity"; @@ -13,7 +14,6 @@ function MPSeries(props) { const [config, setConfig] = useState(null); - console.log('PROPS: '+ days); useEffect(() => { const fetchConfig = async () => { try { @@ -99,29 +99,7 @@ function MPSeries(props) { > -
-
- -
MOST POPULAR SERIES
-
Users
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.Name}

-

{item.unique_viewers}

-
- - ))} - -
-
+ diff --git a/src/pages/components/statCards/mv_libraries.js b/src/pages/components/statCards/mv_libraries.js index 3bea621..ce8ed81 100644 --- a/src/pages/components/statCards/mv_libraries.js +++ b/src/pages/components/statCards/mv_libraries.js @@ -1,9 +1,9 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; -import Config from "../../../lib/config"; import ComponentLoading from "../ComponentLoading"; +import StatComponent from "./statsComponent"; import TvLineIcon from "remixicon-react/TvLineIcon"; import FilmLineIcon from "remixicon-react/FilmLineIcon"; @@ -11,23 +11,12 @@ import FilmLineIcon from "remixicon-react/FilmLineIcon"; function MVLibraries(props) { const [data, setData] = useState([]); const [days, setDays] = useState(30); - const [config, setConfig] = useState(null); + useEffect(() => { - const fetchConfig = async () => { - try { - const newConfig = await Config(); - setConfig(newConfig); - } catch (error) { - if (error.code === "ERR_NETWORK") { - console.log(error); - } - } - }; const fetchLibraries = () => { - if (config) { const url = `/stats/getMostViewedLibraries`; axios @@ -42,13 +31,9 @@ function MVLibraries(props) { .catch((error) => { console.log(error); }); - } }; - if (!config) { - fetchConfig(); - } if (!data || data.length===0) { fetchLibraries(); @@ -60,7 +45,7 @@ function MVLibraries(props) { const intervalId = setInterval(fetchLibraries, 60000 * 5); return () => clearInterval(intervalId); - }, [data, config, days,props.days]); + }, [data, days,props.days]); if (!data) { return( @@ -89,29 +74,7 @@ function MVLibraries(props) { -
-
- -
MOST VIEWED LIBRARIES
-
Plays
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.Name}

-

{item.Plays}

-
- - ))} - -
-
+ diff --git a/src/pages/components/statCards/mv_movies.js b/src/pages/components/statCards/mv_movies.js index a0d9c73..7248c56 100644 --- a/src/pages/components/statCards/mv_movies.js +++ b/src/pages/components/statCards/mv_movies.js @@ -3,9 +3,10 @@ import axios from "axios"; import Config from "../../../lib/config"; import ComponentLoading from "../ComponentLoading"; +import StatComponent from "./statsComponent"; -function MVMovies(props) { +function MVMusic(props) { const [data, setData] = useState([]); const [days, setDays] = useState(30); @@ -98,33 +99,11 @@ function MVMovies(props) { > -
-
- -
MOST VIEWED MOVIES
-
Plays
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.Name}

-

{item.Plays}

-
- - ))} - -
-
+ ); } -export default MVMovies; +export default MVMusic; diff --git a/src/pages/components/statCards/mv_music.js b/src/pages/components/statCards/mv_music.js new file mode 100644 index 0000000..1b05632 --- /dev/null +++ b/src/pages/components/statCards/mv_music.js @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; + +import ComponentLoading from "../ComponentLoading"; +import StatComponent from "./statsComponent"; + + +function MVMovies(props) { + const [data, setData] = useState([]); + const [days, setDays] = useState(30); + + const [config, setConfig] = useState(null); + + + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config(); + setConfig(newConfig); + } catch (error) { + if (error.code === "ERR_NETWORK") { + console.log(error); + } + } + }; + + const fetchLibraries = () => { + if (config) { + const url = `/stats/getMostViewedMusic`; + + axios + .post(url, {days:props.days}, { + headers: { + "Content-Type": "application/json", + }, + }) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + + if (!config) { + fetchConfig(); + } + + if (!data || data.length===0) { + fetchLibraries(); + } + if (days !== props.days) { + setDays(props.days); + fetchLibraries(); + } + + + const intervalId = setInterval(fetchLibraries, 60000 * 5); + return () => clearInterval(intervalId); + }, [data, config, days,props.days]); + + if (!data) { + return( +
+ +
+ ); + } + if (data.length === 0) { + return <>; + } + + + + return ( +
+ +
+ + +
+ + + +
+ ); +} + +export default MVMovies; diff --git a/src/pages/components/statCards/mv_series.js b/src/pages/components/statCards/mv_series.js index ecda9cd..0b0a2a3 100644 --- a/src/pages/components/statCards/mv_series.js +++ b/src/pages/components/statCards/mv_series.js @@ -4,6 +4,8 @@ import Config from "../../../lib/config"; import ComponentLoading from "../ComponentLoading"; +import StatComponent from "./statsComponent"; + // import PlaybackActivity from "./components/playbackactivity"; function MVSeries(props) { @@ -98,29 +100,7 @@ function MVSeries(props) { > -
-
- -
MOST VIEWED SERIES
-
Plays
-
- -
- - {data && - data - .map((item,index) => ( - -
-

{(index+1)}

-

{item.Name}

-

{item.Plays}

-
- - ))} - -
-
+ diff --git a/src/pages/components/statCards/statsComponent.js b/src/pages/components/statCards/statsComponent.js new file mode 100644 index 0000000..7116382 --- /dev/null +++ b/src/pages/components/statCards/statsComponent.js @@ -0,0 +1,25 @@ +import React from "react"; + +function StatComponent(props) { + return ( +
+
+
{props.heading}
+
{props.units}
+
+ +
+ {props.data && + props.data.map((item, index) => ( +
+

{index + 1}

+

{item.Name || item.Client}

+

{item.Plays || item.unique_viewers}

+
+ ))} +
+
+ ); +} + +export default StatComponent; diff --git a/src/pages/css/error.css b/src/pages/css/error.css new file mode 100644 index 0000000..40274ee --- /dev/null +++ b/src/pages/css/error.css @@ -0,0 +1,18 @@ +.error +{ + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.error .message +{ + color:crimson; + font-size: 1.5em; + font-weight: 500; +} \ No newline at end of file diff --git a/src/pages/css/navbar.css b/src/pages/css/navbar.css new file mode 100644 index 0000000..f76c9dc --- /dev/null +++ b/src/pages/css/navbar.css @@ -0,0 +1,36 @@ +.navbar { + display: flex; + justify-content: flex-end; + align-items: center; + background-color: rgb(10,25,41); + height: 50px; +} + + +.navitem { + display: flex; + align-items: center; + height: 100%; + color: white; + font-size: 16px; + text-decoration: none; + padding: 0 20px; + transition: background-color 0.2s ease-in-out; +} + + +.nav-text { + margin-left: 10px; +} + +.active +{ + background-color: #308df046 !important; + transition: background-color 0.2s ease-in-out; +} + +.navitem:hover { + background-color: #326aa541; + +} + diff --git a/src/pages/css/sessions.css b/src/pages/css/sessions.css index 306d12d..a27138b 100644 --- a/src/pages/css/sessions.css +++ b/src/pages/css/sessions.css @@ -1,4 +1,9 @@ .sessions { + margin-bottom: 10px; + +} + +.sessions-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(520px, 520px)); grid-auto-rows: 235px;/* max-width+offset so 215 + 20*/ @@ -9,7 +14,6 @@ } - .session-card { display: flex; color: white; diff --git a/src/pages/css/sidenav.css b/src/pages/css/sidenav.css deleted file mode 100644 index a0d78ec..0000000 --- a/src/pages/css/sidenav.css +++ /dev/null @@ -1,75 +0,0 @@ -.sidenav { - width: 250px; - transition: width 0.3s ease-in-out; - height: 100vh; - /* z-index: 100; */ - top:0; - background-color: rgb(10,25,41); - /* padding-top: 28px; */ - position: sticky; -} - -.active -{ - background-color: #308df046 !important; - transition: background-color 0.2s ease-in-out; -} -.Closed { - /* composes: sidenav; */ - transition: width 0.3s ease-in-out; - width: 60px; - /* padding-top: 28px; */ - /* position: absolute; */ - -} -.sideitem { - display: flex; - align-items: center; - padding: 10px 20px; - cursor: pointer; - color: #B2BAC2; - text-decoration: none; - overflow: hidden; -} -.text-open { - padding-left: 16px; - opacity: 1; - transition: opacity 0.3s ease-in; -} - -.text-closed { - /* padding-left: 16px; */ - opacity: 0; - transition: opacity 0.3s ease-in; - -} - -.sideitem:hover { - background-color: #244f7d1c; - -} -.menuBtn { - align-self: center; - align-self: flex-start; - justify-self: flex-end; - color: #B2BAC2; - background-color: transparent; - border: none; - cursor: pointer; - padding-left: 10px; - width: inherit; - transition: width 0.3s ease-in; - -} - -.menuBtn:hover { - - background-color: #244f7d1c; - -} - -.menuBtn-open { - width: 100%; - transition: width 0.3s ease-in; - -} \ No newline at end of file diff --git a/src/pages/css/statCard.css b/src/pages/css/statCard.css index e0d7e62..f9fb17d 100644 --- a/src/pages/css/statCard.css +++ b/src/pages/css/statCard.css @@ -1,11 +1,13 @@ .Heading { display: flex; + margin-bottom: 10px; } .Heading h1 { padding-right: 10px; + margin: 0; } .stat-cards-container @@ -127,10 +129,11 @@ height: 35px; color: white; display: flex; - background-color: rgb(0, 99, 248,0.6); + background-color: rgb(0, 0, 0,0.2); border-radius: 4px; font-size: 1.2em; - align-self: center; + align-self: flex-end; + justify-content: space-evenly; } @@ -165,6 +168,6 @@ input[type=number] { .date-range .header, .date-range .trailer { - padding: 5px; - text-align: center; + padding-inline: 10px; + align-self: center; } diff --git a/src/pages/css/users.css b/src/pages/css/users.css new file mode 100644 index 0000000..d9f53bf --- /dev/null +++ b/src/pages/css/users.css @@ -0,0 +1,160 @@ +.Users +{ + color: white; + padding-right: 20px; + padding-bottom: 50px; +} + +.user-activity-table { + border-collapse: collapse; + border-radius: 5px; + /* margin: 25px 0; */ + font-size: 0.9em; + font-family: sans-serif; + min-width: 400px; + /* box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); */ + background-color: rgba(0, 0, 0, 0.2); + color: white; + width: 100%; + +} + + th, + td + { + padding: 10px 15px; + /* text-align: left; */ + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + border-right: 1px solid rgba(255, 255, 255, 0.05); + border-left:1px solid rgba(255, 255, 255, 0.05); +} + +td a{ + text-decoration: none; + color: white; +} + +td:hover a{ + + color: rgb(0, 164, 219); +} + +th { + background-color: rgba(0, 0, 0, 0.8); + cursor: pointer; +} + +th:hover { + border-bottom: 1px solid rgba(255, 255, 255, 0.5); +} + + + +/* tbody tr:last-of-type { + border-bottom: 2px solid #009879; +} */ + + +.card-user-image +{ + border-radius: 50%; + width: 30px; + height: 30px; + object-fit: cover; + +} + +tbody tr:hover +{ + background-color: rgba(0, 0, 0, 0.4); +} + + +td:first-child { + border-left: none; + } + + td:last-child { + border-right: none; + } + + + .pagination { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: 1rem; + } + + .page-btn { + padding: 5px 10px; + border: none; + border-radius: 5px; + margin-inline: 5px; + background-color: rgb(10,25,41); + color: white; + cursor: pointer; + } + + .page-btn:hover { + background-color: #333; + color: #fff; + } + + .page-btn:disabled { + opacity: 0.5; + cursor: default; + } + + .page-number { + margin-inline: 10px; + font-weight: bold; + } + + +.pagination-range +{ + width: 130px; + height: 35px; + color: white; + display: flex; + background-color: rgb(0, 0, 0,0.2); + border-radius: 4px; + font-size: 1.2em; + align-self: flex-end; + justify-content: space-between; +} + +.pagination-range select +{ + + height: 35px; + outline: none; + border: none; + border-radius: 4px; + background-color: rgb(255, 255, 255, 0.1); + color:white; + font-size: 1em; + + +} +.pagination-range .header +{ + padding-inline: 10px; + align-self: center; +} + + +select option { + background-color: #4a4a4a; + outline: unset; + width: 100%; + border: none; + +} + +.pagination-range .items +{ + background-color: rgb(255, 255, 255, 0.1); + padding-inline: 10px; +} \ No newline at end of file diff --git a/src/pages/css/usersactivity.css b/src/pages/css/usersactivity.css deleted file mode 100644 index 9e8744a..0000000 --- a/src/pages/css/usersactivity.css +++ /dev/null @@ -1,67 +0,0 @@ -.Users -{ - color: white; - padding-right: 20px; - padding-bottom: 50px; -} - -.user-activity-table { - border-collapse: collapse; - border-radius: 5px; - /* margin: 25px 0; */ - font-size: 0.9em; - font-family: sans-serif; - min-width: 400px; - box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); - color: white; - width: 100%; - -} - - th, -td { - padding: 12px 15px; - /* text-align: left; */ - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - border-right: 1px solid rgba(255, 255, 255, 0.05); - border-left:1px solid rgba(255, 255, 255, 0.05); -} - -th { - background-color: rgba(0, 0, 0, 0.5); - cursor: pointer; -} - -th:hover { - border-bottom: 1px solid rgba(255, 255, 255, 0.5); -} - - - -/* tbody tr:last-of-type { - border-bottom: 2px solid #009879; -} */ - - -.card-user-image -{ - border-radius: 50%; - width: 30px; - height: 30px; - object-fit: cover; - -} - -tr:hover -{ - background-color: rgba(0, 0, 0, 0.2); -} - - -td:first-child { - border-left: none; - } - - td:last-child { - border-right: none; - } diff --git a/src/pages/css/websocket/websocket.css b/src/pages/css/websocket/websocket.css index 7e18cdf..148ebc9 100644 --- a/src/pages/css/websocket/websocket.css +++ b/src/pages/css/websocket/websocket.css @@ -17,3 +17,25 @@ margin: 0; font-family: monospace; } + + +.console-container { + overflow: auto; /* show scrollbar when needed */ +} + +.console-container::-webkit-scrollbar { + width: 10px; /* set scrollbar width */ +} + +.console-container::-webkit-scrollbar-track { + background-color: transparent; /* set track color */ +} + +.console-container::-webkit-scrollbar-thumb { + background-color: #8888884d; /* set thumb color */ + border-radius: 5px; /* round corners */ +} + +.console-container::-webkit-scrollbar-thumb:hover { + background-color: #88888883; /* set thumb color */ +} \ No newline at end of file diff --git a/src/pages/home.js b/src/pages/home.js index eb09af3..ba68167 100644 --- a/src/pages/home.js +++ b/src/pages/home.js @@ -3,7 +3,7 @@ import React from 'react' import './css/home.css' import Sessions from './components/sessions' -import StatCards from './components/StatsCards' +import WatchStatistics from './components/WatchStatistics' import LibraryOverView from './components/libraryOverview' @@ -12,7 +12,7 @@ export default function Home() {
- +
diff --git a/src/pages/libraries.js b/src/pages/libraries.js index fdc7b9e..e963d8a 100644 --- a/src/pages/libraries.js +++ b/src/pages/libraries.js @@ -3,7 +3,7 @@ import axios from "axios"; import Config from "../lib/config"; import "./css/libraries.css"; -import "./css/usersactivity.css"; +import "./css/users.css"; import Loading from "./components/loading"; diff --git a/src/pages/settings.js b/src/pages/settings.js index c9727db..ce8470b 100644 --- a/src/pages/settings.js +++ b/src/pages/settings.js @@ -2,7 +2,8 @@ import React from "react"; import SettingsConfig from "./components/settings/settingsConfig"; import LibrarySync from "./components/settings/librarySync"; -import WebSocketComponent from "./components/settings/WebSocketComponent "; + +import TerminalComponent from "./components/settings/TerminalComponent"; import "./css/settings.css"; @@ -13,7 +14,7 @@ export default function Settings() {
- +
); diff --git a/src/pages/setup.js b/src/pages/setup.js index 2a97ed1..93a41e6 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -15,6 +15,59 @@ function Setup() { function handleFormChange(event) { setFormValues({ ...formValues, [event.target.name]: event.target.value }); } + async function beginSync() { + + + setProcessing(true); + + await axios + .get("/sync/writeLibraries") + .then((response) => { + if (response.status === 200) { + // isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); + + await axios + .get("/sync/writeLibraryItems") + .then((response) => { + if (response.status === 200) { + // isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); + + + await axios + .get("/sync/writeSeasonsAndEpisodes") + .then((response) => { + if (response.status === 200) { + // isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); + + + await axios + .get("/sync/writeUsers") + .then((response) => { + if (response.status === 200) { + // isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); + setProcessing(false); + // return { isValid: isValid, errorMessage: errorMessage }; + } async function validateSettings(_url, _apikey) { // Send a GET request to /system/configuration to test copnnection @@ -73,12 +126,13 @@ function Setup() { "Content-Type": "application/json", }, }) - .then((response) => { + .then(async (response) => { setsubmitButtonText("Settings Saved"); setProcessing(false); setTimeout(() => { window.location.href = "/"; }, 1000); + await beginSync(); return; }) diff --git a/src/pages/userdata.js b/src/pages/testing.js similarity index 93% rename from src/pages/userdata.js rename to src/pages/testing.js index 740ebab..bf85914 100644 --- a/src/pages/userdata.js +++ b/src/pages/testing.js @@ -4,7 +4,7 @@ import './css/libraries.css'; import Loading from './components/loading'; // import PlaybackActivity from './components/playbackactivity'; - +import Activity from './activity'; // import StatCards from './components/StatsCards'; import LibraryOverView from './components/libraryOverview'; @@ -12,7 +12,7 @@ import LibraryOverView from './components/libraryOverview'; import API from '../classes/jellyfin-api'; -function UserData() { +function Testing() { const [data, setData] = useState([]); useEffect(() => { @@ -49,4 +49,4 @@ function UserData() { ); } -export default UserData; +export default Testing; diff --git a/src/pages/user-info.js b/src/pages/user-info.js new file mode 100644 index 0000000..f076800 --- /dev/null +++ b/src/pages/user-info.js @@ -0,0 +1,14 @@ +import { useParams } from 'react-router-dom'; + +function UserInfo() { + const { UserId } = useParams(); + + // Fetch data for the user with the specified userId + + return ( +
+

{UserId}

+
+ ); +} +export default UserInfo; diff --git a/src/pages/useractivity.js b/src/pages/useractivity.js deleted file mode 100644 index 74e541e..0000000 --- a/src/pages/useractivity.js +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState, useEffect } from "react"; -import axios from "axios"; -import Config from "../lib/config"; - -import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon"; - -import "./css/usersactivity.css"; - -import Loading from "./components/loading"; - -function UserActivity() { - const [data, setData] = useState([]); - const [config, setConfig] = useState(null); - const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); - - function handleSort(key) { - const direction = - sortConfig.key === key && sortConfig.direction === "ascending" - ? "descending" - : "ascending"; - setSortConfig({ key, direction }); - } - - function sortData(data, { key, direction }) { - if (!key) return data; - - const sortedData = [...data]; - - sortedData.sort((a, b) => { - if (a[key] < b[key]) return direction === "ascending" ? -1 : 1; - if (a[key] > b[key]) return direction === "ascending" ? 1 : -1; - return 0; - }); - - return sortedData; - } - - useEffect(() => { - const fetchConfig = async () => { - try { - const newConfig = await Config(); - setConfig(newConfig); - } catch (error) { - if (error.code === "ERR_NETWORK") { - console.log(error); - } - } - }; - - const fetchData = () => { - if (config) { - const url = `${config.hostUrl}/user_usage_stats/user_activity?days=9999`; - const apiKey = config.apiKey; - - axios - .get(url, { - headers: { - "X-MediaBrowser-Token": apiKey, - }, - }) - .then((data) => { - console.log("data"); - setData(data.data); - console.log(data); - }) - .catch((error) => { - console.log(error); - }); - } - }; - - if (!config) { - fetchConfig(); - } - - if (data.length === 0) { - fetchData(); - } - - const intervalId = setInterval(fetchData, 60000); - return () => clearInterval(intervalId); - }, [data, config]); - - if (!data || data.length === 0) { - return ; - } - const sortedData = sortData(data, sortConfig); - return ( -
-

Users

- - - - - - - - - - - - - - {sortedData.map((item) => ( - - - - - - - - - - ))} - -
handleSort("user_name")}>User handleSort("item_name")}>Last Watched handleSort("client_name")}>Last Client handleSort("total_count")}>Total Plays handleSort("total_play_time")}> - Total Watch Time - handleSort("last_seen")}>Last Seen
- {item.has_image ? ( - - ) : ( - - )} - {item.user_name}{item.item_name}{item.client_name}{item.total_count}{item.total_play_time}{item.last_seen} ago
-
- ); -} - -export default UserActivity; diff --git a/src/pages/users.js b/src/pages/users.js new file mode 100644 index 0000000..eb08a28 --- /dev/null +++ b/src/pages/users.js @@ -0,0 +1,190 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../lib/config"; +import { Link } from 'react-router-dom'; +import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon"; + +import "./css/users.css"; + +import Loading from "./components/loading"; + +function Users() { + const [data, setData] = useState([]); + const [config, setConfig] = useState(null); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); + const [currentPage, setCurrentPage] = useState(1); + const [itemCount,setItemCount] = useState(10); + + function handleSort(key) { + const direction = + sortConfig.key === key && sortConfig.direction === "ascending" + ? "descending" + : "ascending"; + setSortConfig({ key, direction }); + } + + function sortData(data, { key, direction }) { + if (!key) return data; + + const sortedData = [...data]; + + sortedData.sort((a, b) => { + if (a[key] < b[key]) return direction === "ascending" ? -1 : 1; + if (a[key] > b[key]) return direction === "ascending" ? 1 : -1; + return 0; + }); + + return sortedData; + } + + function formatTime(time) { + const units = { + days: ['Day', 'Days'], + hours: ['Hour', 'Hours'], + minutes: ['Minute', 'Minutes'], + seconds: ['Second', 'Seconds'] + }; + + let formattedTime = ''; + + for (const unit in units) { + if (time[unit]) { + const unitName = units[unit][time[unit] > 1 ? 1 : 0]; + formattedTime += `${time[unit]} ${unitName} `; + } + } + + return `${formattedTime}ago`; + } + + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config(); + setConfig(newConfig); + } catch (error) { + if (error.code === "ERR_NETWORK") { + console.log(error); + } + } + }; + + const fetchData = () => { + if (config) { + const url = `/stats/getAllUserActivity`; + + axios + .get(url) + .then((data) => { + console.log(data); + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + if (!config) { + fetchConfig(); + } + + if (data.length === 0) { + fetchData(); + } + + const intervalId = setInterval(fetchData, 60000); + return () => clearInterval(intervalId); + }, [data, config]); + + if (!data || data.length === 0) { + return ; + } + + const sortedData = sortData(data, sortConfig); + + const indexOfLastUser = currentPage * itemCount; + const indexOfFirstUser = indexOfLastUser - itemCount; + const currentUsers = sortedData.slice(indexOfFirstUser, indexOfLastUser); + + const pageNumbers = []; + for (let i = 1; i <= Math.ceil(sortedData.length / itemCount); i++) { + pageNumbers.push(i); + } + + + return ( +
+
+

All Users

+
+
Items
+ +
+
+ + + + + + + + + + + + + + + {currentUsers.map((item) => ( + + + + + + + + + + ))} + +
handleSort("UserName")}>User handleSort("LastWatched")}>Last Watched handleSort("LastClient")}>Last Client handleSort("TotalPlays")}>Total Plays handleSort("TotalWatchTime")}>Total Watch Time handleSort("LastSeen")}>Last Seen
+ {item.PrimaryImageTag ? ( + + ) : ( + + )} + {item.UserName}{item.LastWatched || 'never'}{item.LastClient || 'n/a'}{item.TotalPlays}{item.TotalWatchTime || 0}{item.LastSeen ? formatTime(item.LastSeen) : 'never'}
+
+ + +
{`Page ${currentPage} of ${pageNumbers.length}`}
+ + +
+
+ ); +} +export default Users;