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`. --> -
{index + 1}
-{item.Name}
-{item.Library_Count}
-{index + 1}
-{item.Name}
-{item.Library_Count} / {item.Season_Count} / {item.Episode_Count}
-{index + 1}
+{item.Name}
+{item.CollectionType =='tvshows'? (item.Library_Count+' / '+item.Season_Count+' / '+item.Episode_Count): item.Library_Count}
+{(index+1)}
-{item.UserName}
-{item.Plays}
-{(index+1)}
-{item.Client}
-{item.Plays}
-{(index+1)}
-{item.Name}
-{item.unique_viewers}
-{(index+1)}
-{item.Name}
-{item.unique_viewers}
-{(index+1)}
-{item.Name}
-{item.Plays}
-{(index+1)}
-{item.Name}
-{item.Plays}
-{(index+1)}
-{item.Name}
-{item.Plays}
-{index + 1}
+{item.Name || item.Client}
+{item.Plays || item.unique_viewers}
+{UserId}
+| - | 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 | -
| + | 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'} | +