mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
full change to statistics
1) Created components for statistic reporting. 2) Database changes and PROC/Function creations. Still need to make MOST VIEWED LIBRARIES/CLIENTS/ MOST ACTIVE USERS dynamically load with date range (Function Creation on DB side)
This commit is contained in:
13
SQL Scripts/1. CREATE DATABASE jfstat.sql
Normal file
13
SQL Scripts/1. CREATE DATABASE jfstat.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Database: jfstat
|
||||
|
||||
-- DROP DATABASE IF EXISTS jfstat;
|
||||
|
||||
CREATE DATABASE jfstat
|
||||
WITH
|
||||
OWNER = jfstat
|
||||
ENCODING = 'UTF8'
|
||||
LC_COLLATE = 'en_US.utf8'
|
||||
LC_CTYPE = 'en_US.utf8'
|
||||
TABLESPACE = pg_default
|
||||
CONNECTION LIMIT = -1
|
||||
IS_TEMPLATE = False;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Table: public.jf_activity_watchdog
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_activity_watchdog;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_activity_watchdog
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"IsPaused" boolean DEFAULT false,
|
||||
"UserId" text COLLATE pg_catalog."default",
|
||||
"UserName" text COLLATE pg_catalog."default",
|
||||
"Client" text COLLATE pg_catalog."default",
|
||||
"DeviceName" text COLLATE pg_catalog."default",
|
||||
"DeviceId" text COLLATE pg_catalog."default",
|
||||
"ApplicationVersion" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemId" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemName" text COLLATE pg_catalog."default",
|
||||
"SeasonId" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
"EpisodeId" text COLLATE pg_catalog."default",
|
||||
"PlaybackDuration" bigint,
|
||||
"ActivityDateInserted" timestamp with time zone
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_activity_watchdog
|
||||
OWNER to postgres;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- Table: public.jf_library_episodes
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_library_episodes;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_library_episodes
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"EpisodeId" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default",
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"PremiereDate" timestamp with time zone,
|
||||
"OfficialRating" text COLLATE pg_catalog."default",
|
||||
"CommunityRating" double precision,
|
||||
"RunTimeTicks" bigint,
|
||||
"ProductionYear" integer,
|
||||
"IndexNumber" integer,
|
||||
"ParentIndexNumber" integer,
|
||||
"Type" text COLLATE pg_catalog."default",
|
||||
"ParentLogoItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropImageTags" text COLLATE pg_catalog."default",
|
||||
"SeriesId" text COLLATE pg_catalog."default",
|
||||
"SeasonId" text COLLATE pg_catalog."default",
|
||||
"SeasonName" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT jf_library_episodes_pkey PRIMARY KEY ("Id")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_library_episodes
|
||||
OWNER to postgres;
|
||||
18
SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql
Normal file
18
SQL Scripts/1. CREATE TABLES/CREATE TABLE app_config.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Table: public.app_config
|
||||
|
||||
-- DROP TABLE IF EXISTS public.app_config;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.app_config
|
||||
(
|
||||
"ID" integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
|
||||
"JF_HOST" text COLLATE pg_catalog."default",
|
||||
"JF_API_KEY" text COLLATE pg_catalog."default",
|
||||
"APP_USER" text COLLATE pg_catalog."default",
|
||||
"APP_PASSWORD" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT app_config_pkey PRIMARY KEY ("ID")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.app_config
|
||||
OWNER to postgres;
|
||||
20
SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql
Normal file
20
SQL Scripts/1. CREATE TABLES/CREATE TABLE jf_libraries.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Table: public.jf_libraries
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_libraries;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_libraries
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"IsFolder" boolean NOT NULL DEFAULT true,
|
||||
"Type" text COLLATE pg_catalog."default" NOT NULL DEFAULT 'CollectionFolder'::text,
|
||||
"CollectionType" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"ImageTagsPrimary" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT jf_libraries_pkey PRIMARY KEY ("Id")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_libraries
|
||||
OWNER to postgres;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Table: public.jf_library_items
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_library_items;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_library_items
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"PremiereDate" timestamp with time zone,
|
||||
"EndDate" timestamp with time zone,
|
||||
"CommunityRating" double precision,
|
||||
"RunTimeTicks" bigint,
|
||||
"ProductionYear" integer,
|
||||
"IsFolder" boolean,
|
||||
"Type" text COLLATE pg_catalog."default",
|
||||
"Status" text COLLATE pg_catalog."default",
|
||||
"ImageTagsPrimary" text COLLATE pg_catalog."default",
|
||||
"ImageTagsBanner" text COLLATE pg_catalog."default",
|
||||
"ImageTagsLogo" text COLLATE pg_catalog."default",
|
||||
"ImageTagsThumb" text COLLATE pg_catalog."default",
|
||||
"BackdropImageTags" text COLLATE pg_catalog."default",
|
||||
"ParentId" text COLLATE pg_catalog."default" NOT NULL,
|
||||
CONSTRAINT jf_library_items_pkey PRIMARY KEY ("Id"),
|
||||
CONSTRAINT jf_library_items_fkey FOREIGN KEY ("ParentId")
|
||||
REFERENCES public.jf_libraries ("Id") MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE SET NULL
|
||||
NOT VALID
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_library_items
|
||||
OWNER to postgres;
|
||||
|
||||
COMMENT ON CONSTRAINT jf_library_items_fkey ON public.jf_library_items
|
||||
IS 'jf_library';
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Table: public.jf_library_seasons
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_library_seasons;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_library_seasons
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"Name" text COLLATE pg_catalog."default",
|
||||
"ServerId" text COLLATE pg_catalog."default",
|
||||
"IndexNumber" integer,
|
||||
"Type" text COLLATE pg_catalog."default",
|
||||
"ParentLogoItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropItemId" text COLLATE pg_catalog."default",
|
||||
"ParentBackdropImageTags" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
"SeriesId" text COLLATE pg_catalog."default",
|
||||
"SeriesPrimaryImageTag" text COLLATE pg_catalog."default",
|
||||
CONSTRAINT jf_library_seasons_pkey PRIMARY KEY ("Id")
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_library_seasons
|
||||
OWNER to postgres;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Table: public.jf_playback_activity
|
||||
|
||||
-- DROP TABLE IF EXISTS public.jf_playback_activity;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.jf_playback_activity
|
||||
(
|
||||
"Id" text COLLATE pg_catalog."default" NOT NULL,
|
||||
"IsPaused" boolean DEFAULT false,
|
||||
"UserId" text COLLATE pg_catalog."default",
|
||||
"UserName" text COLLATE pg_catalog."default",
|
||||
"Client" text COLLATE pg_catalog."default",
|
||||
"DeviceName" text COLLATE pg_catalog."default",
|
||||
"DeviceId" text COLLATE pg_catalog."default",
|
||||
"ApplicationVersion" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemId" text COLLATE pg_catalog."default",
|
||||
"NowPlayingItemName" text COLLATE pg_catalog."default",
|
||||
"SeasonId" text COLLATE pg_catalog."default",
|
||||
"SeriesName" text COLLATE pg_catalog."default",
|
||||
"EpisodeId" text COLLATE pg_catalog."default",
|
||||
"PlaybackDuration" bigint,
|
||||
"ActivityDateInserted" timestamp with time zone
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public.jf_playback_activity
|
||||
OWNER to postgres;
|
||||
@@ -0,0 +1,45 @@
|
||||
-- FUNCTION: public.fs_most_played_items(integer, text)
|
||||
|
||||
-- DROP FUNCTION IF EXISTS public.fs_most_played_items(integer, text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_most_played_items(
|
||||
days integer,
|
||||
itemtype text)
|
||||
RETURNS TABLE("Plays" bigint, total_playback_duration numeric, "Name" text, "Id" text)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.plays,
|
||||
t.total_playback_duration,
|
||||
i."Name",
|
||||
i."Id"
|
||||
FROM (
|
||||
SELECT
|
||||
count(*) AS plays,
|
||||
sum(jf_playback_activity."PlaybackDuration") AS total_playback_duration,
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
FROM
|
||||
jf_playback_activity
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
GROUP BY
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY
|
||||
count(*) DESC
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON t."NowPlayingItemId" = i."Id"
|
||||
AND i."Type" = itemType
|
||||
ORDER BY
|
||||
t.plays DESC;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
ALTER FUNCTION public.fs_most_played_items(integer, text)
|
||||
OWNER TO postgres;
|
||||
@@ -0,0 +1,52 @@
|
||||
-- FUNCTION: public.fs_most_popular_items(integer, text)
|
||||
|
||||
-- DROP FUNCTION IF EXISTS public.fs_most_popular_items(integer, text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_most_popular_items(
|
||||
days integer,
|
||||
itemtype text)
|
||||
RETURNS TABLE(unique_viewers bigint, latest_activity_date timestamp with time zone, "Name" text, "Id" text)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
t.unique_viewers,
|
||||
t.latest_activity_date,
|
||||
i."Name",
|
||||
i."Id"
|
||||
FROM (
|
||||
SELECT
|
||||
jf_playback_activity."NowPlayingItemId",
|
||||
count(DISTINCT jf_playback_activity."UserId") AS unique_viewers,
|
||||
latest_activity_date.latest_date AS latest_activity_date
|
||||
FROM
|
||||
jf_playback_activity
|
||||
JOIN (
|
||||
SELECT
|
||||
jf_playback_activity_1."NowPlayingItemId",
|
||||
max(jf_playback_activity_1."ActivityDateInserted") AS latest_date
|
||||
FROM
|
||||
jf_playback_activity jf_playback_activity_1
|
||||
GROUP BY jf_playback_activity_1."NowPlayingItemId"
|
||||
) latest_activity_date
|
||||
ON jf_playback_activity."NowPlayingItemId" = latest_activity_date."NowPlayingItemId"
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
GROUP BY
|
||||
jf_playback_activity."NowPlayingItemId", latest_activity_date.latest_date
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON t."NowPlayingItemId" = i."Id"
|
||||
AND i."Type" = itemType
|
||||
ORDER BY
|
||||
t.unique_viewers DESC, t.latest_activity_date DESC;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
ALTER FUNCTION public.fs_most_popular_items(integer, text)
|
||||
OWNER TO postgres;
|
||||
@@ -0,0 +1,48 @@
|
||||
-- FUNCTION: public.fs_most_viewed_libraries(integer)
|
||||
|
||||
-- DROP FUNCTION IF EXISTS public.fs_most_viewed_libraries(integer);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_most_viewed_libraries(
|
||||
days integer)
|
||||
RETURNS TABLE("Plays" numeric, "Id" text, "Name" text, "ServerId" text, "IsFolder" boolean, "Type" text, "CollectionType" text, "ImageTagsPrimary" text)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
sum(t."Plays"),
|
||||
l."Id",
|
||||
l."Name",
|
||||
l."ServerId",
|
||||
l."IsFolder",
|
||||
l."Type",
|
||||
l."CollectionType",
|
||||
l."ImageTagsPrimary"
|
||||
FROM (
|
||||
SELECT count(*) AS "Plays",
|
||||
sum(jf_playback_activity."PlaybackDuration") AS "TotalPlaybackDuration",
|
||||
jf_playback_activity."NowPlayingItemId"
|
||||
FROM jf_playback_activity
|
||||
WHERE
|
||||
jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => days) and NOW()
|
||||
|
||||
GROUP BY jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY "Plays" DESC
|
||||
) t
|
||||
JOIN jf_library_items i
|
||||
ON i."Id" = t."NowPlayingItemId"
|
||||
JOIN jf_libraries l
|
||||
ON l."Id" = i."ParentId"
|
||||
GROUP BY
|
||||
l."Id"
|
||||
ORDER BY
|
||||
(sum( t."Plays")) DESC;
|
||||
END;
|
||||
$BODY$;
|
||||
|
||||
ALTER FUNCTION public.fs_most_viewed_libraries(integer)
|
||||
OWNER TO postgres;
|
||||
11
SQL Scripts/2. CREATE USER jfstat.sql
Normal file
11
SQL Scripts/2. CREATE USER jfstat.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Role: jfstat
|
||||
-- DROP ROLE IF EXISTS jfstat;
|
||||
|
||||
CREATE ROLE jfstat WITH
|
||||
LOGIN
|
||||
SUPERUSER
|
||||
INHERIT
|
||||
CREATEDB
|
||||
CREATEROLE
|
||||
NOREPLICATION
|
||||
ENCRYPTED PASSWORD 'SCRAM-SHA-256$4096:Cf1EY3ozsXG1sR/TWv/Xcw==$Om2f07jurCEEyaOGV/Fju9AGtUVj67Q1JFm0AZSiK4M=:lFaFNHdvtEHzC8l5qUf/uAWENJHa1T9jM3Bv5WDz66E=';
|
||||
@@ -0,0 +1,22 @@
|
||||
-- View: public.jf_library_count_view
|
||||
|
||||
-- DROP VIEW public.jf_library_count_view;
|
||||
|
||||
CREATE OR REPLACE VIEW public.jf_library_count_view
|
||||
AS
|
||||
SELECT l."Id",
|
||||
l."Name",
|
||||
l."CollectionType",
|
||||
count(DISTINCT i."Id") AS "Library_Count",
|
||||
count(DISTINCT s."Id") AS "Season_Count",
|
||||
count(DISTINCT e."Id") AS "Episode_Count"
|
||||
FROM jf_libraries l
|
||||
JOIN jf_library_items i ON i."ParentId" = l."Id"
|
||||
LEFT JOIN jf_library_seasons s ON s."SeriesId" = i."Id"
|
||||
LEFT JOIN jf_library_episodes e ON e."SeasonId" = s."Id"
|
||||
GROUP BY l."Id", l."Name"
|
||||
ORDER BY (count(DISTINCT i."Id")) DESC;
|
||||
|
||||
ALTER TABLE public.jf_library_count_view
|
||||
OWNER TO postgres;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- View: public.js_most_active_user
|
||||
|
||||
-- DROP VIEW public.js_most_active_user;
|
||||
|
||||
CREATE OR REPLACE VIEW public.js_most_active_user
|
||||
AS
|
||||
SELECT count(*) AS "Plays",
|
||||
jf_playback_activity."UserId",
|
||||
jf_playback_activity."UserName"
|
||||
FROM jf_playback_activity
|
||||
GROUP BY jf_playback_activity."UserId", jf_playback_activity."UserName"
|
||||
ORDER BY (count(*)) DESC;
|
||||
|
||||
ALTER TABLE public.js_most_active_user
|
||||
OWNER TO postgres;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- View: public.js_most_used_clients
|
||||
|
||||
-- DROP VIEW public.js_most_used_clients;
|
||||
|
||||
CREATE OR REPLACE VIEW public.js_most_used_clients
|
||||
AS
|
||||
SELECT count(*) AS "Plays",
|
||||
jf_playback_activity."Client"
|
||||
FROM jf_playback_activity
|
||||
GROUP BY jf_playback_activity."Client"
|
||||
ORDER BY (count(*)) DESC;
|
||||
|
||||
ALTER TABLE public.js_most_used_clients
|
||||
OWNER TO postgres;
|
||||
|
||||
@@ -12,7 +12,7 @@ router.get("/test", async (req, res) => {
|
||||
|
||||
router.get("/getconfig", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
console.log(`ENDPOINT CALLED: /getconfig: ` + rows);
|
||||
// console.log(`ENDPOINT CALLED: /getconfig: ` + rows);
|
||||
// console.log(`ENDPOINT CALLED: /setconfig: `+rows.length);
|
||||
res.send(rows);
|
||||
});
|
||||
@@ -43,7 +43,20 @@ router.get("/getAllFromJellyfin", async (req, res) => {
|
||||
|
||||
res.send(results);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getAllFromJellyfin: `);
|
||||
// console.log(`ENDPOINT CALLED: /getAllFromJellyfin: `);
|
||||
});
|
||||
|
||||
|
||||
router.post("/getLibraryItems", async (req, res) => {
|
||||
const Id = req.headers['id'];
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT * FROM jf_library_items where "ParentId"='${Id}'`
|
||||
);
|
||||
console.log({ Id: Id });
|
||||
res.send(rows);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getLibraryItems: `);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,14 +1,82 @@
|
||||
// db.js
|
||||
const { Pool } = require('pg');
|
||||
const pgp = require("pg-promise")();
|
||||
|
||||
const pool = new Pool({
|
||||
user: 'jfstat',
|
||||
host: '10.0.0.99',
|
||||
database: 'jfstat',
|
||||
password: '123456',
|
||||
port: 32778, // or your PostgreSQL port number
|
||||
});
|
||||
user: 'jfstat',
|
||||
host: '10.0.0.99',
|
||||
database: 'jfstat',
|
||||
password: '123456',
|
||||
port: 32778, // or your PostgreSQL port number
|
||||
});
|
||||
|
||||
async function deleteBulk(table_name, data) {
|
||||
const client = await pool.connect();
|
||||
let result='SUCCESS';
|
||||
let message='';
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// const AllIds = data.map((row) => row.Id);
|
||||
|
||||
if (data.length !== 0) {
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM ${table_name} WHERE "Id" IN (${pgp.as.csv(
|
||||
data
|
||||
)})`
|
||||
};
|
||||
// console.log(deleteQuery);
|
||||
await client.query(deleteQuery);
|
||||
}
|
||||
// else {
|
||||
// await client.query(`DELETE FROM ${table_name}`);
|
||||
// console.log('Delete All');
|
||||
// }
|
||||
|
||||
await client.query('COMMIT');
|
||||
message=(data.length + " Rows removed.");
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
message=('Error: '+ error);
|
||||
result='ERROR';
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
return ({Result:result,message:message});
|
||||
}
|
||||
|
||||
async function insertBulk(table_name, data,columns) {
|
||||
const client = await pool.connect();
|
||||
let result='SUCCESS';
|
||||
let message='';
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
data,
|
||||
columns,
|
||||
table_name
|
||||
);
|
||||
await client.query(query);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
message=(data.length + " Rows Inserted.");
|
||||
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
message=('Error: '+ error);
|
||||
result='ERROR';
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
return ({Result:result,message:message});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
query: (text, params) => pool.query(text, params),
|
||||
deleteBulk: deleteBulk,
|
||||
insertBulk: insertBulk,
|
||||
};
|
||||
|
||||
41
backend/models/jf_activity_watchdog.js
Normal file
41
backend/models/jf_activity_watchdog.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const jf_activity_watchdog_columns = [
|
||||
"Id",
|
||||
"IsPaused",
|
||||
"UserId",
|
||||
"UserName",
|
||||
"Client",
|
||||
"DeviceName",
|
||||
"DeviceId",
|
||||
"ApplicationVersion",
|
||||
"NowPlayingItemId",
|
||||
"NowPlayingItemName",
|
||||
"EpisodeId",
|
||||
"SeasonId",
|
||||
"SeriesName",
|
||||
"PlaybackDuration",
|
||||
"ActivityDateInserted",
|
||||
];
|
||||
|
||||
|
||||
const jf_activity_watchdog_mapping = (item) => ({
|
||||
Id: item.Id ,
|
||||
IsPaused: item.PlayState.IsPaused !== undefined ? item.PlayState.IsPaused : item.IsPaused,
|
||||
UserId: item.UserId,
|
||||
UserName: item.UserName,
|
||||
Client: item.Client,
|
||||
DeviceName: item.DeviceName,
|
||||
DeviceId: item.DeviceId,
|
||||
ApplicationVersion: item.ApplicationVersion,
|
||||
NowPlayingItemId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.SeriesId : item.NowPlayingItem.Id,
|
||||
NowPlayingItemName: item.NowPlayingItem.Name,
|
||||
EpisodeId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.Id: null,
|
||||
SeasonId: item.NowPlayingItem.SeasonId || null,
|
||||
SeriesName: item.NowPlayingItem.SeriesName || null,
|
||||
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0,
|
||||
ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_activity_watchdog_columns,
|
||||
jf_activity_watchdog_mapping
|
||||
};
|
||||
26
backend/models/jf_libraries.js
Normal file
26
backend/models/jf_libraries.js
Normal file
@@ -0,0 +1,26 @@
|
||||
////////////////////////// pn delete move to playback
|
||||
const jf_libraries_columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"CollectionType",
|
||||
"ImageTagsPrimary",
|
||||
];
|
||||
|
||||
const jf_libraries_mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
CollectionType: item.CollectionType,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_libraries_columns,
|
||||
jf_libraries_mapping,
|
||||
};
|
||||
52
backend/models/jf_library_episodes.js
Normal file
52
backend/models/jf_library_episodes.js
Normal file
@@ -0,0 +1,52 @@
|
||||
////////////////////////// pn delete move to playback
|
||||
const jf_library_episodes_columns = [
|
||||
"Id",
|
||||
"EpisodeId",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"OfficialRating",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IndexNumber",
|
||||
"ParentIndexNumber",
|
||||
"Type",
|
||||
"ParentLogoItemId",
|
||||
"ParentBackdropItemId",
|
||||
"ParentBackdropImageTags",
|
||||
"SeriesId",
|
||||
"SeasonId",
|
||||
"SeasonName",
|
||||
"SeriesName",
|
||||
];
|
||||
|
||||
const jf_library_episodes_mapping = (item) => ({
|
||||
Id: item.Id + item.ParentId,
|
||||
EpisodeId: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
OfficialRating: item.OfficialRating,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IndexNumber: item.IndexNumber,
|
||||
ParentIndexNumber: item.ParentIndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:
|
||||
item.ParentBackdropImageTags !== undefined
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesId: item.SeriesId,
|
||||
SeasonId: item.ParentId,
|
||||
SeasonName: item.SeasonName,
|
||||
SeriesName: item.SeriesName,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_library_episodes_columns,
|
||||
jf_library_episodes_mapping,
|
||||
};
|
||||
49
backend/models/jf_library_items.js
Normal file
49
backend/models/jf_library_items.js
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
const jf_library_items_columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"EndDate",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"Status",
|
||||
"ImageTagsPrimary",
|
||||
"ImageTagsBanner",
|
||||
"ImageTagsLogo",
|
||||
"ImageTagsThumb",
|
||||
"BackdropImageTags",
|
||||
"ParentId",
|
||||
];
|
||||
|
||||
const jf_library_items_mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner:
|
||||
item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo:
|
||||
item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb:
|
||||
item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_library_items_columns,
|
||||
jf_library_items_mapping,
|
||||
};
|
||||
36
backend/models/jf_library_seasons.js
Normal file
36
backend/models/jf_library_seasons.js
Normal file
@@ -0,0 +1,36 @@
|
||||
////////////////////////// pn delete move to playback
|
||||
const jf_library_seasons_columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"IndexNumber",
|
||||
"Type",
|
||||
"ParentLogoItemId",
|
||||
"ParentBackdropItemId",
|
||||
"ParentBackdropImageTags",
|
||||
"SeriesName",
|
||||
"SeriesId",
|
||||
"SeriesPrimaryImageTag",
|
||||
];
|
||||
|
||||
const jf_library_seasons_mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IndexNumber: item.IndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:
|
||||
item.ParentBackdropImageTags !== undefined
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesName: item.SeriesName,
|
||||
SeriesId: item.ParentId,
|
||||
SeriesPrimaryImageTag: item.SeriesPrimaryImageTag,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_library_seasons_columns,
|
||||
jf_library_seasons_mapping,
|
||||
};
|
||||
42
backend/models/jf_playback_activity.js
Normal file
42
backend/models/jf_playback_activity.js
Normal file
@@ -0,0 +1,42 @@
|
||||
////////////////////////// pn delete move to playback
|
||||
const columnsPlayback = [
|
||||
"Id",
|
||||
"IsPaused",
|
||||
"UserId",
|
||||
"UserName",
|
||||
"Client",
|
||||
"DeviceName",
|
||||
"DeviceId",
|
||||
"ApplicationVersion",
|
||||
"NowPlayingItemId",
|
||||
"NowPlayingItemName",
|
||||
"EpisodeId",
|
||||
"SeasonId",
|
||||
"SeriesName",
|
||||
"PlaybackDuration",
|
||||
"ActivityDateInserted",
|
||||
];
|
||||
|
||||
|
||||
const mappingPlayback = (item) => ({
|
||||
Id: item.Id ,
|
||||
IsPaused: item.PlayState.IsPaused !== undefined ? item.PlayState.IsPaused : item.IsPaused,
|
||||
UserId: item.UserId,
|
||||
UserName: item.UserName,
|
||||
Client: item.Client,
|
||||
DeviceName: item.DeviceName,
|
||||
DeviceId: item.DeviceId,
|
||||
ApplicationVersion: item.ApplicationVersion,
|
||||
NowPlayingItemId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.SeriesId : item.NowPlayingItem.Id,
|
||||
NowPlayingItemName: item.NowPlayingItem.Name,
|
||||
EpisodeId: item.NowPlayingItem.SeriesId !== undefined ? item.NowPlayingItem.Id: null,
|
||||
SeasonId: item.NowPlayingItem.SeasonId || null,
|
||||
SeriesName: item.NowPlayingItem.SeriesName || null,
|
||||
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration: 0,
|
||||
ActivityDateInserted: item.ActivityDateInserted !== undefined ? item.ActivityDateInserted: new Date().toISOString(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
columnsPlayback,
|
||||
mappingPlayback,
|
||||
};
|
||||
@@ -4,6 +4,8 @@ const cors = require('cors');
|
||||
const apiRouter = require('./api');
|
||||
const syncRouter = require('./sync');
|
||||
const statsRouter = require('./stats');
|
||||
const ActivityMonitor=require('./watchdog/ActivityMonitor');
|
||||
ActivityMonitor.ActivityMonitor(1000);
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3003;
|
||||
|
||||
@@ -15,4 +15,99 @@ router.get("/getLibraryOverview", async (req, res) => {
|
||||
console.log(`ENDPOINT CALLED: /getLibraryOverview`);
|
||||
});
|
||||
|
||||
|
||||
router.post("/getMostViewedSeries", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_played_items(${_days},'Series') limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
|
||||
router.post("/getMostViewedMovies", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_played_items(${_days},'Movie') limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.post("/getMostViewedLibraries", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_viewed_libraries(${_days})`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
router.get("/getMostUsedClient", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM js_most_used_clients limit 5');
|
||||
res.send(rows);
|
||||
});
|
||||
|
||||
router.get("/getMostActiveUsers", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM js_most_active_user limit 5');
|
||||
res.send(rows);
|
||||
});
|
||||
|
||||
|
||||
router.post("/getMostPopularMovies", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_popular_items(${_days},'Movie') limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.post("/getMostPopularSeries", async (req, res) => {
|
||||
const {days} = req.body;
|
||||
let _days=days;
|
||||
if(days===undefined)
|
||||
{
|
||||
_days=30;
|
||||
}
|
||||
console.log(`select * from fs_most_popular_items(${_days},'Series') limit 5`);
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_most_popular_items(${_days},'Series') limit 5`
|
||||
);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
|
||||
router.get("/getPlaybackActivity", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM jf_playback_activity');
|
||||
res.send(rows);
|
||||
// console.log(`ENDPOINT CALLED: /getPlaybackActivity`);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
421
backend/sync.js
421
backend/sync.js
@@ -8,7 +8,22 @@ const sendMessageToClients = ws(8080);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
const {
|
||||
jf_libraries_columns,
|
||||
jf_libraries_mapping,
|
||||
} = require("./models/jf_libraries");
|
||||
const {
|
||||
jf_library_items_columns,
|
||||
jf_library_items_mapping,
|
||||
} = require("./models/jf_library_items");
|
||||
const {
|
||||
jf_library_seasons_columns,
|
||||
jf_library_seasons_mapping,
|
||||
} = require("./models/jf_library_seasons");
|
||||
const {
|
||||
jf_library_episodes_columns,
|
||||
jf_library_episodes_mapping,
|
||||
} = require("./models/jf_library_episodes");
|
||||
|
||||
/////////////////////////////////////////Functions
|
||||
class sync {
|
||||
@@ -95,49 +110,32 @@ router.get("/writeLibraries", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
sendMessageToClients({Message:"Error: Config details not found!" });
|
||||
sendMessageToClients({ Message: "Error: Config details not found!" });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const data = await _sync.getItem(); //getting all root folders aka libraries
|
||||
|
||||
const columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"CollectionType",
|
||||
"ImageTagsPrimary",
|
||||
]; // specify the columns to insert into
|
||||
// specify the columns to insert into
|
||||
|
||||
const existingIds = await db
|
||||
.query('SELECT "Id" FROM jf_libraries')
|
||||
.then((res) => res.rows.map((row) => row.Id)); // get existing library Ids from the db
|
||||
|
||||
//data mapping
|
||||
const mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
CollectionType: item.CollectionType,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
});
|
||||
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
dataToInsert = await data.map(mapping);
|
||||
dataToInsert = await data.map(jf_libraries_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
dataToInsert = await data
|
||||
.filter((row) => !existingIds.includes(row.Id))
|
||||
.map(mapping);
|
||||
.map(jf_libraries_mapping);
|
||||
}
|
||||
|
||||
//Bulkinsert new data not on db
|
||||
@@ -147,7 +145,11 @@ router.get("/writeLibraries", async (req, res) => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(dataToInsert, columns, "jf_libraries");
|
||||
const query = pgp.helpers.insert(
|
||||
dataToInsert,
|
||||
jf_libraries_columns,
|
||||
"jf_libraries"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
@@ -162,12 +164,14 @@ router.get("/writeLibraries", async (req, res) => {
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
sendMessageToClients({Message:"Error performing bulk insert:" + error});
|
||||
sendMessageToClients({
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk insert" });
|
||||
sendMessageToClients({Message:"No new data to bulk insert"});
|
||||
sendMessageToClients({ Message: "No new data to bulk insert" });
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIds.length > data.length) {
|
||||
@@ -193,7 +197,9 @@ router.get("/writeLibraries", async (req, res) => {
|
||||
Type: "Success",
|
||||
Message: existingIds.length - data.length + " Rows Removed.",
|
||||
});
|
||||
sendMessageToClients(existingIds.length - data.length + " Rows Removed.");
|
||||
sendMessageToClients(
|
||||
existingIds.length - data.length + " Rows Removed."
|
||||
);
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
@@ -201,16 +207,17 @@ router.get("/writeLibraries", async (req, res) => {
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
sendMessageToClients({Message:"Error performing bulk removal:" + error});
|
||||
sendMessageToClients({
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk delete" });
|
||||
sendMessageToClients({Message:"No new data to bulk delete"});
|
||||
sendMessageToClients({ Message: "No new data to bulk delete" });
|
||||
}
|
||||
//Sent logs
|
||||
|
||||
|
||||
res.send(message);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraries: `);
|
||||
@@ -228,11 +235,18 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
}
|
||||
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/2" });
|
||||
|
||||
sendMessageToClients({
|
||||
color: "yellow",
|
||||
Message: "Beginning Library Item Sync",
|
||||
});
|
||||
//Get all Library items
|
||||
//gets all libraries
|
||||
const libraries = await _sync.getItem();
|
||||
const data = [];
|
||||
let insertCounter = 0;
|
||||
let deleteCounter = 0;
|
||||
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
|
||||
for (let i = 0; i < libraries.length; i++) {
|
||||
const item = libraries[i];
|
||||
@@ -250,62 +264,19 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
.query('SELECT "Id" FROM jf_library_items')
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
//Mappings to store data in DB
|
||||
const columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"EndDate",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"Status",
|
||||
"ImageTagsPrimary",
|
||||
"ImageTagsBanner",
|
||||
"ImageTagsLogo",
|
||||
"ImageTagsThumb",
|
||||
"BackdropImageTags",
|
||||
"ParentId",
|
||||
]; // specify the columns to insert into
|
||||
|
||||
//data mapping
|
||||
const mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner:
|
||||
item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo:
|
||||
item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb:
|
||||
item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId,
|
||||
});
|
||||
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
dataToInsert = await data.map(mapping);
|
||||
dataToInsert = await data.map(jf_library_items_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
dataToInsert = await data
|
||||
.filter((row) => !existingIds.includes(row.Id))
|
||||
.map(mapping);
|
||||
.map(jf_library_items_mapping);
|
||||
}
|
||||
|
||||
//Bulkinsert new data not on db
|
||||
@@ -317,7 +288,7 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
dataToInsert,
|
||||
columns,
|
||||
jf_library_items_columns,
|
||||
"jf_library_items"
|
||||
);
|
||||
await db.query(query);
|
||||
@@ -327,12 +298,17 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
Type: "Success",
|
||||
Message: dataToInsert.length + " Rows Inserted.",
|
||||
});
|
||||
insertCounter += dataToInsert.length;
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing Item insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
@@ -362,6 +338,7 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
Type: "Success",
|
||||
Message: existingIds.length - data.length + " Rows Removed.",
|
||||
});
|
||||
deleteCounter += existingIds.length - data.length;
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
@@ -369,13 +346,28 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing Item removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk delete" });
|
||||
// sendMessageToClients({Message:"No new Library items to bulk delete"});
|
||||
}
|
||||
//Sent logs
|
||||
|
||||
sendMessageToClients({
|
||||
color: "dodgerblue",
|
||||
Message: insertCounter + " Library Items Inserted.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "orange",
|
||||
Message: deleteCounter + " Library Items Removed.",
|
||||
});
|
||||
sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
|
||||
|
||||
res.send(message);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraryItems: `);
|
||||
@@ -383,7 +375,11 @@ router.get("/writeLibraryItems", async (req, res) => {
|
||||
|
||||
//////////////////////////////////////////////////////writeSeasonsAndEpisodes
|
||||
router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
sendMessageToClients({color:'yellow',Message:"Beginning Seasons and Episode sync"});
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/2" });
|
||||
sendMessageToClients({
|
||||
color: "yellow",
|
||||
Message: "Beginning Seasons and Episode sync",
|
||||
});
|
||||
const message = [];
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
@@ -398,6 +394,10 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
`SELECT * FROM public.jf_library_items where "Type"='Series'`
|
||||
);
|
||||
|
||||
let insertSeasonsCount = 0;
|
||||
let insertEpisodeCount = 0;
|
||||
let deleteSeasonsCount = 0;
|
||||
let deleteEpisodeCount = 0;
|
||||
//loop for each show
|
||||
for (const show of shows) {
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id);
|
||||
@@ -405,17 +405,12 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
//
|
||||
//get existing seasons and episodes
|
||||
console.log(show.Id);
|
||||
const existingIdsSeasons = await db
|
||||
.query(
|
||||
`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`
|
||||
)
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
let existingIdsEpisodes = [];
|
||||
if (existingIdsSeasons.length > 0) {
|
||||
existingIdsEpisodes = await db
|
||||
.query(
|
||||
`SELECT * FROM public.jf_library_episodes WHERE "SeasonId" IN (${existingIdsSeasons
|
||||
.query(`SELECT * FROM public.jf_library_episodes WHERE "SeasonId" IN (${existingIdsSeasons
|
||||
.filter((seasons) => seasons !== "")
|
||||
.map((seasons) => pgp.as.value(seasons))
|
||||
.map((value) => "'" + value + "'")
|
||||
@@ -424,85 +419,6 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
}
|
||||
|
||||
//Mappings to store data in DB
|
||||
const columnSeasons = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"IndexNumber",
|
||||
"Type",
|
||||
"ParentLogoItemId",
|
||||
"ParentBackdropItemId",
|
||||
"ParentBackdropImageTags",
|
||||
"SeriesName",
|
||||
"SeriesId",
|
||||
"SeriesPrimaryImageTag",
|
||||
]; // specify the columns to insert into
|
||||
const columnEpisodes = [
|
||||
"Id",
|
||||
"EpisodeId",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"OfficialRating",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IndexNumber",
|
||||
"ParentIndexNumber",
|
||||
"Type",
|
||||
"ParentLogoItemId",
|
||||
"ParentBackdropItemId",
|
||||
"ParentBackdropImageTags",
|
||||
"SeriesId",
|
||||
"SeasonId",
|
||||
"SeasonName",
|
||||
"SeriesName",
|
||||
]; // specify the columns to insert into
|
||||
|
||||
//data mapping
|
||||
const seasonsmapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IndexNumber: item.IndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:
|
||||
item.ParentBackdropImageTags !== undefined
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesName: item.SeriesName,
|
||||
SeriesId: item.ParentId,
|
||||
SeriesPrimaryImageTag: item.SeriesPrimaryImageTag,
|
||||
});
|
||||
|
||||
const episodemapping = (item) => ({
|
||||
Id: item.Id + item.ParentId,
|
||||
EpisodeId: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
OfficialRating: item.OfficialRating,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IndexNumber: item.IndexNumber,
|
||||
ParentIndexNumber: item.ParentIndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:
|
||||
item.ParentBackdropImageTags !== undefined
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesId: item.SeriesId,
|
||||
SeasonId: item.ParentId,
|
||||
SeasonName: item.SeasonName,
|
||||
SeriesName: item.SeriesName,
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
let seasonsToInsert = [];
|
||||
@@ -511,58 +427,46 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
|
||||
if (existingIdsSeasons.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
seasonsToInsert = await data.allSeasons.map(seasonsmapping);
|
||||
seasonsToInsert = await data.allSeasons.map(jf_library_seasons_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
seasonsToInsert = await data.allSeasons
|
||||
.filter((row) => !existingIdsSeasons.includes(row.Id))
|
||||
.map(seasonsmapping);
|
||||
.map(jf_library_seasons_mapping);
|
||||
}
|
||||
|
||||
if (existingIdsEpisodes.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
episodesToInsert = await data.allEpisodes.map(episodemapping);
|
||||
episodesToInsert = await data.allEpisodes.map(jf_library_episodes_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
episodesToInsert = await data.allEpisodes
|
||||
.filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId))
|
||||
.map(episodemapping);
|
||||
episodesToInsert = await data.allEpisodes.filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId)).map(jf_library_episodes_mapping);
|
||||
}
|
||||
|
||||
///insert delete seasons
|
||||
//Bulkinsert new data not on db
|
||||
if (seasonsToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
seasonsToInsert,
|
||||
columnSeasons,
|
||||
"jf_library_seasons"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: seasonsToInsert.length + " Rows Inserted for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'cornflowerblue',Message:seasonsToInsert.length + " Rows Inserted for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error});
|
||||
}
|
||||
})();
|
||||
let result = await db.insertBulk("jf_library_seasons",seasonsToInsert,jf_library_seasons_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: seasonsToInsert.length + " Rows Inserted for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
insertSeasonsCount += seasonsToInsert.length;
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
@@ -570,51 +474,38 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk insert for " + show.Name});
|
||||
}
|
||||
|
||||
const toDeleteIds = existingIdsSeasons.filter((id) =>!data.allSeasons.some((row) => row.Id === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIdsSeasons.length > data.allSeasons.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
if (toDeleteIds.length > 0) {
|
||||
|
||||
const AllIds = data.allSeasons.map((row) => row.Id);
|
||||
|
||||
let table="jf_library_seasons";
|
||||
let result = await db.deleteBulk(table,toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: toDeleteIds.length + " Rows Removed for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: table,
|
||||
});
|
||||
deleteSeasonsCount +=toDeleteIds.length;
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: result.message,
|
||||
ItemId: show.Id,
|
||||
TableName: table,
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: result.message,
|
||||
});
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_library_seasons WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message:
|
||||
existingIdsSeasons.length -
|
||||
data.allSeasons.length +
|
||||
" Rows Removed for " +
|
||||
show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'orange',Message:existingIdsSeasons.length -data.allSeasons.length +" Rows Removed for " +show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error});
|
||||
}
|
||||
})();
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
@@ -622,7 +513,7 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
// sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
}
|
||||
//insert delete episodes
|
||||
//Bulkinsert new data not on db
|
||||
@@ -634,7 +525,7 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
episodesToInsert,
|
||||
columnEpisodes,
|
||||
jf_library_episodes_columns,
|
||||
"jf_library_episodes"
|
||||
);
|
||||
await db.query(query);
|
||||
@@ -647,7 +538,8 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'cornflowerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name});
|
||||
insertEpisodeCount += episodesToInsert.length;
|
||||
// sendMessageToClients({color:'dodgerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
@@ -656,7 +548,10 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
@@ -666,7 +561,7 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk insert for " + show.Name});
|
||||
// sendMessageToClients({Message:"No new data to bulk insert for " + show.Name});
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIdsEpisodes.length > data.allEpisodes.length) {
|
||||
@@ -698,7 +593,9 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name});
|
||||
deleteEpisodeCount +=
|
||||
existingIdsEpisodes.length - data.allEpisodes.length;
|
||||
// sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
@@ -708,7 +605,10 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error});
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
@@ -718,10 +618,29 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
// sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
}
|
||||
|
||||
sendMessageToClients({ Message: "Sync complete for " + show.Name });
|
||||
}
|
||||
sendMessageToClients({color:'lightgreen',Message:"Sync Complete"});
|
||||
|
||||
sendMessageToClients({
|
||||
color: "dodgerblue",
|
||||
Message: insertSeasonsCount + " Seasons inserted.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "orange",
|
||||
Message: deleteSeasonsCount + " Seasons Removed.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "dodgerblue",
|
||||
Message: insertEpisodeCount + " Episodes inserted.",
|
||||
});
|
||||
sendMessageToClients({
|
||||
color: "orange",
|
||||
Message: deleteEpisodeCount + " Episodes Removed.",
|
||||
});
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
|
||||
res.send(message);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `);
|
||||
|
||||
170
backend/watchdog/ActivityMonitor.js
Normal file
170
backend/watchdog/ActivityMonitor.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const db = require("../db");
|
||||
const pgp = require("pg-promise")();
|
||||
const axios = require("axios");
|
||||
const { columnsPlayback, mappingPlayback } = require('../models/jf_playback_activity');
|
||||
const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require('../models/jf_activity_watchdog');
|
||||
|
||||
async function ActivityMonitor(interval) {
|
||||
console.log("Activity Interval: " + interval);
|
||||
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
const base_url = config[0].JF_HOST;
|
||||
const apiKey = config[0].JF_API_KEY;
|
||||
|
||||
if (base_url === null || config[0].JF_API_KEY === null) {
|
||||
console.log("Config Details Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const url = `${base_url}/Sessions`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": apiKey,
|
||||
},
|
||||
});
|
||||
const SessionData=response.data.filter(row => row.NowPlayingItem !== undefined);
|
||||
|
||||
/////get data from jf_activity_monitor
|
||||
const WatchdogData=await db.query('SELECT * FROM jf_activity_watchdog').then((res) => res.rows);
|
||||
|
||||
// //compare to sessiondata
|
||||
|
||||
let WatchdogDataToInsert = [];
|
||||
let WatchdogDataToUpdate = [];
|
||||
//filter fix if table is empty
|
||||
|
||||
if (WatchdogData.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
WatchdogDataToInsert = await SessionData.map(jf_activity_watchdog_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
WatchdogDataToInsert = await SessionData.filter((session) => !WatchdogData.map((wdData) => wdData.Id).includes(session.Id))
|
||||
.map(jf_activity_watchdog_mapping);
|
||||
|
||||
WatchdogDataToUpdate = WatchdogData.filter((wdData) => {
|
||||
const session = SessionData.find((sessionData) => sessionData.Id === wdData.Id);
|
||||
if (session && session.PlayState) {
|
||||
if (wdData.IsPaused !== session.PlayState.IsPaused) {
|
||||
wdData.IsPaused = session.PlayState.IsPaused;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
// console.log(WatchdogDataToUpdate);
|
||||
|
||||
if (WatchdogDataToInsert.length !== 0) {
|
||||
db.insertBulk("jf_activity_watchdog",WatchdogDataToInsert,jf_activity_watchdog_columns);
|
||||
}
|
||||
|
||||
|
||||
//update wd state
|
||||
if(WatchdogDataToUpdate.length>0)
|
||||
{
|
||||
|
||||
|
||||
const WatchdogDataUpdated = WatchdogDataToUpdate.map(obj => {
|
||||
|
||||
const startTime = new Date(obj.ActivityDateInserted);
|
||||
const endTime =new Date();
|
||||
const diffInSeconds = Math.floor((endTime - startTime) / 1000);
|
||||
|
||||
if(obj.IsPaused) {
|
||||
obj.PlaybackDuration =parseInt(obj.PlaybackDuration)+ diffInSeconds;
|
||||
}
|
||||
|
||||
obj.ActivityDateInserted = `to_timestamp('${new Date().toISOString()}', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
|
||||
const {...rest } = obj;
|
||||
|
||||
return { ...rest };
|
||||
});
|
||||
|
||||
|
||||
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
const cs = new pgp.helpers.ColumnSet([
|
||||
'?Id',
|
||||
'IsPaused',
|
||||
{ name: 'PlaybackDuration', mod: ':raw' },
|
||||
{ name: 'ActivityDateInserted', mod: ':raw' },
|
||||
]);
|
||||
|
||||
const updateQuery = pgp.helpers.update(WatchdogDataUpdated, cs,'jf_activity_watchdog' ) + ' WHERE v."Id" = t."Id"';
|
||||
await db.query(updateQuery)
|
||||
.then(result => {
|
||||
console.log('Update successful', result.rowCount, 'rows updated');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating rows', error);
|
||||
});
|
||||
|
||||
await db.query("COMMIT");
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
console.log(error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
//delete from db no longer in session data and insert into stats db (still to make)
|
||||
//Bulk delete from db thats no longer on api
|
||||
|
||||
const toDeleteIds = WatchdogData.filter((id) =>!SessionData.some((row) => row.Id === id.Id)).map((row) => row.Id);
|
||||
|
||||
|
||||
const playbackData = WatchdogData.filter((id) => !SessionData.some((row) => row.Id === id.Id));
|
||||
|
||||
|
||||
const playbackToInsert = playbackData.map(obj => {
|
||||
|
||||
const startTime = new Date(obj.ActivityDateInserted);
|
||||
const endTime =new Date();
|
||||
const diffInSeconds = Math.floor((endTime - startTime) / 1000);
|
||||
|
||||
if(!obj.IsPaused) {
|
||||
obj.PlaybackDuration =parseInt(obj.PlaybackDuration)+ diffInSeconds;
|
||||
}
|
||||
obj.ActivityDateInserted = new Date().toISOString();
|
||||
const {...rest } = obj;
|
||||
|
||||
return { ...rest };
|
||||
});
|
||||
|
||||
|
||||
|
||||
if(toDeleteIds.length>0)
|
||||
{
|
||||
let result=await db.deleteBulk('jf_activity_watchdog',toDeleteIds)
|
||||
console.log(result);
|
||||
}
|
||||
if(playbackToInsert.length>0)
|
||||
{
|
||||
let result=await db.insertBulk('jf_playback_activity',playbackToInsert,columnsPlayback);
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////
|
||||
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ActivityMonitor,
|
||||
};
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"axios": "^1.3.4",
|
||||
"concurrently": "^7.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"pg": "^8.9.0",
|
||||
"pg-promise": "^11.3.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"axios": "^1.3.4",
|
||||
"concurrently": "^7.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"pg": "^8.9.0",
|
||||
"pg-promise": "^11.3.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
67
src/App.css
67
src/App.css
@@ -28,34 +28,6 @@ h1{
|
||||
color: white;
|
||||
}
|
||||
|
||||
ul{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li.old {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
animation-name: fade-out;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
li.new {
|
||||
|
||||
border: 2px solid grey;
|
||||
border-radius: 5px;
|
||||
padding:10px 10px;
|
||||
margin:20px 20px 0px 0px;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
animation-name: fade-in;
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -68,19 +40,6 @@ li.new {
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.ActivityDetail
|
||||
{
|
||||
font-size: large;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.ActivityTime
|
||||
{
|
||||
/* text-align: right; */
|
||||
font-size: small;
|
||||
font-style: italic;
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
|
||||
.App-header {
|
||||
@@ -110,29 +69,3 @@ li.new {
|
||||
}
|
||||
|
||||
|
||||
|
||||
.Activity
|
||||
{
|
||||
|
||||
/* max-width: 50%; */
|
||||
/* border: 1px solid white; */
|
||||
border-radius: 5px;
|
||||
/* margin-left: 50px;
|
||||
margin-right: 50px; */
|
||||
|
||||
/* grid-area: "⌛"; */
|
||||
|
||||
/* background-color: #282c34; */
|
||||
}
|
||||
|
||||
.Activity ul
|
||||
{
|
||||
list-style-type: none !important;
|
||||
color: white;
|
||||
/* background-color: #282c34; */
|
||||
}
|
||||
/* *
|
||||
{
|
||||
outline: 1px solid green;
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import API from "../classes/jellyfin-api";
|
||||
|
||||
import "../App.css";
|
||||
import "./css/activity.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
@@ -12,7 +12,7 @@ function Activity() {
|
||||
let _api = new API();
|
||||
|
||||
const fetchData = () => {
|
||||
_api.getActivityData(30).then((ActivityData) => {
|
||||
_api.getActivityData(15).then((ActivityData) => {
|
||||
if (data && data.length > 0) {
|
||||
const newDataOnly = ActivityData.Items.filter((item) => {
|
||||
return !data.some((existingItem) => existingItem.Id === item.Id);
|
||||
@@ -46,15 +46,16 @@ function Activity() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Activity">
|
||||
<h1>Activity Log</h1>
|
||||
<div>
|
||||
<h1>Activity</h1>
|
||||
<div className="Activity">
|
||||
<ul>
|
||||
{data &&
|
||||
data.map((item) => (
|
||||
<li
|
||||
key={item.Id}
|
||||
className={
|
||||
data.findIndex((items) => items.Id === item.Id) <= 30
|
||||
data.findIndex((items) => items.Id === item.Id) <= 15
|
||||
? "new"
|
||||
: "old"
|
||||
}
|
||||
@@ -68,6 +69,8 @@ function Activity() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
12
src/pages/components/ComponentLoading.js
Normal file
12
src/pages/components/ComponentLoading.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import "../css/loading.css";
|
||||
|
||||
function ComponentLoading() {
|
||||
return (
|
||||
<div className="component-loading">
|
||||
<div className="loading__spinner"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ComponentLoading;
|
||||
61
src/pages/components/StatsCards.js
Normal file
61
src/pages/components/StatsCards.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import MVLibraries from "./statCards/mv_libraries";
|
||||
import MVMovies from "./statCards/mv_movies";
|
||||
import MVSeries from "./statCards/mv_series";
|
||||
import MostUsedClient from "./statCards/most_used_client";
|
||||
import MostActiveUsers from "./statCards/most_active_users";
|
||||
import MPSeries from "./statCards/mp_series";
|
||||
import MPMovies from "./statCards/mp_movies";
|
||||
import "../css/statCard.css";
|
||||
|
||||
function StatCards() {
|
||||
const [days, setDays] = useState(30);
|
||||
const [input, setInput] = useState(30);
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
if (input < 1) {
|
||||
setInput(1);
|
||||
setDays(0);
|
||||
} else {
|
||||
setDays(parseInt(input) - 1);
|
||||
}
|
||||
|
||||
console.log(days);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div className="Heading">
|
||||
<h1>Watch Statistics</h1>
|
||||
<div className="date-range">
|
||||
<div className="header">Last</div>
|
||||
<div className="days">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="trailer">Days</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div className="stat-cards-container">
|
||||
<MVMovies days={days} />
|
||||
<MPMovies days={days} />
|
||||
<MVSeries days={days} />
|
||||
<MPSeries days={days} />
|
||||
<MVLibraries days={days} />
|
||||
<MostUsedClient days={days} />
|
||||
<MostActiveUsers days={days} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatCards;
|
||||
@@ -4,22 +4,26 @@ import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Loading from "./loading";
|
||||
|
||||
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
|
||||
export default function LibraryOverView() {
|
||||
const [data, setData] = useState([]);
|
||||
const [base_url, setURL] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (base_url === "") {
|
||||
Config()
|
||||
.then((config) => {
|
||||
setURL(config.hostUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
Config()
|
||||
.then((config) => {
|
||||
setURL(config.hostUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
const fetchData = () => {
|
||||
const url = `http://localhost:3003/stats/getLibraryOverview`;
|
||||
const url = `/stats/getLibraryOverview`;
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => setData(response.data))
|
||||
@@ -29,48 +33,77 @@ export default function LibraryOverView() {
|
||||
if (!data || data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
}, [data,base_url]);
|
||||
}, [data, base_url]);
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overview">
|
||||
{data &&
|
||||
data.map((stats) => (
|
||||
<div className="card" style={{
|
||||
backgroundImage: `url(${
|
||||
base_url +
|
||||
"/Items/" +
|
||||
(stats.Isd) +
|
||||
"/Images/Primary?quality=50"
|
||||
})`,
|
||||
}}
|
||||
key={stats.Id}
|
||||
>
|
||||
<div className="item-count">
|
||||
<div>
|
||||
<p>Items in Library</p><p>{stats.Library_Count}</p>
|
||||
</div>
|
||||
{stats.CollectionType === "tvshows" ? (
|
||||
<div>
|
||||
<p>Seasons</p><p>{stats.Season_Count}</p>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{stats.CollectionType === "tvshows" ? (
|
||||
<div>
|
||||
<p>Episodes</p><p>{stats.Episode_Count}</p>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div>
|
||||
<h1>Library Statistics</h1>
|
||||
<div className="overview-container">
|
||||
<div className="library-card">
|
||||
|
||||
<div className="library-image">
|
||||
<div className="library-icons">
|
||||
<TvLineIcon size={"80%"} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="library">
|
||||
<div className="library-header">
|
||||
<div>MOVIE LIBRARIES</div>
|
||||
<div className="library-header-count">MOVIES</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-list">
|
||||
{data &&
|
||||
data.filter((stat) => stat.CollectionType === "movies")
|
||||
.map((item, index) => (
|
||||
<div className="library-item" key={item.Id}>
|
||||
<p className="library-item-index">{index + 1}</p>
|
||||
<p className="library-item-name">{item.Name}</p>
|
||||
<p className="library-item-count">{item.Library_Count}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="library-card">
|
||||
|
||||
<div className="library-image">
|
||||
<div className="library-icons">
|
||||
<FilmLineIcon size={"80%"} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="library">
|
||||
<div className="library-header">
|
||||
<div>SHOW LIBRARIES</div>
|
||||
<div className="library-header-count">
|
||||
SERIES / SEASONS / EPISODES
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-list">
|
||||
{data &&
|
||||
data.filter((stat) => stat.CollectionType === "tvshows")
|
||||
.map((item, index) => (
|
||||
<div className="library-item" key={item.Id}>
|
||||
<p className="library-item-index">{index + 1}</p>
|
||||
<p className="library-item-name">{item.Name}</p>
|
||||
<p className="library-item-count">{item.Library_Count} / {item.Season_Count} / {item.Episode_Count}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
125
src/pages/components/playbackactivity.js
Normal file
125
src/pages/components/playbackactivity.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
// import Config from "../lib/config";
|
||||
|
||||
// import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
|
||||
import "../css/usersactivity.css";
|
||||
|
||||
import Loading from "./loading";
|
||||
|
||||
function PlaybackActivity() {
|
||||
const [data, setData] = useState([]);
|
||||
// const [config, setConfig] = useState(null);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
|
||||
|
||||
function handleSort(key) {
|
||||
const direction =
|
||||
sortConfig.key === key && sortConfig.direction === "ascending"
|
||||
? "descending"
|
||||
: "ascending";
|
||||
setSortConfig({ key, direction });
|
||||
}
|
||||
|
||||
function sortData(data, { key, direction }) {
|
||||
if (!key) return data;
|
||||
|
||||
const sortedData = [...data];
|
||||
|
||||
sortedData.sort((a, b) => {
|
||||
if (a[key] < b[key]) return direction === "ascending" ? -1 : 1;
|
||||
if (a[key] > b[key]) return direction === "ascending" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sortedData;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// const fetchConfig = async () => {
|
||||
// try {
|
||||
// const newConfig = await Config();
|
||||
// setConfig(newConfig);
|
||||
// } catch (error) {
|
||||
// if (error.code === "ERR_NETWORK") {
|
||||
// console.log(error);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
const fetchData = () => {
|
||||
|
||||
axios
|
||||
.get('/stats/getPlaybackActivity')
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
// if (!config) {
|
||||
// fetchConfig();
|
||||
// }
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 10000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data]);
|
||||
|
||||
function convertSecondsToHMS(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${hours}h ${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
const options = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
return (
|
||||
<div className="Users">
|
||||
<h1>Playback Activity</h1>
|
||||
<table className="user-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th onClick={() => handleSort("UserName")}>User</th>
|
||||
<th onClick={() => handleSort("NowPlayingItemName")}>Watched</th>
|
||||
<th onClick={() => handleSort("NowPlayingItemName")}>Episode</th>
|
||||
<th onClick={() => handleSort("PlaybackDuration")}>Playback Duration</th>
|
||||
<th onClick={() => handleSort("ActivityDateInserted")}>Playback Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedData.map((item) => (
|
||||
<tr key={item.user_id}>
|
||||
<td>{item.UserName}</td>
|
||||
<td>{item.SeriesName || item.NowPlayingItemName}</td>
|
||||
<td>{item.SeriesName ? item.NowPlayingItemName: '' }</td>
|
||||
<td>{convertSecondsToHMS(item.PlaybackDuration)}</td>
|
||||
<td>{new Date(item.ActivityDateInserted).toLocaleString("en-GB", options)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlaybackActivity;
|
||||
@@ -98,7 +98,7 @@ function sessionCard(props) {
|
||||
|
||||
return (
|
||||
<div
|
||||
key={props.data.session.Id}
|
||||
|
||||
className="session-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
|
||||
@@ -19,7 +19,9 @@ function Sessions() {
|
||||
const _api = new API();
|
||||
const fetchData = () => {
|
||||
_api.getSessions().then((SessionData) => {
|
||||
setData(SessionData);
|
||||
let results=SessionData.filter((session) => session.NowPlayingItem);
|
||||
setData(results);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
@@ -35,22 +37,34 @@ function Sessions() {
|
||||
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
}, [base_url]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return(<div>
|
||||
<h1>Sessions</h1>
|
||||
<div style={{color:"grey", fontSize:"0.8em", fontStyle:"italic"}}>
|
||||
No Active Sessions Found
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sessions">
|
||||
{data &&
|
||||
data
|
||||
.sort((a, b) =>
|
||||
a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0"))
|
||||
)
|
||||
.map((session) => (
|
||||
<SessionCard data={{ session: session, base_url: base_url }} />
|
||||
))}
|
||||
<div>
|
||||
<h1>Sessions</h1>
|
||||
<div className="sessions">
|
||||
{data &&
|
||||
data
|
||||
.sort((a, b) =>
|
||||
a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0"))
|
||||
)
|
||||
.map((session) => (
|
||||
<SessionCard key={session.Id} data={{ session: session, base_url: base_url }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const WebSocketComponent = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// create a new WebSocket connection
|
||||
const socket = new WebSocket('ws://localhost:8080');
|
||||
const socket = new WebSocket('ws://10.0.0.20:8080');
|
||||
|
||||
// handle incoming messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
@@ -33,7 +33,7 @@ const WebSocketComponent = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <h1>WebSocket Example</h1> */}
|
||||
<h1>Terminal</h1>
|
||||
<div className="console-container">
|
||||
{messages.map((message, index) => (
|
||||
<div key={index} className="console-message">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import axios from "axios";
|
||||
// import Config from "../../../lib/config";
|
||||
// import Loading from "../loading";
|
||||
@@ -6,33 +6,47 @@ import axios from "axios";
|
||||
import "../../css/settings.css";
|
||||
|
||||
export default function LibrarySync() {
|
||||
|
||||
const [processing, setProcessing] = useState(false);
|
||||
async function writeSeasonsAndEpisodes() {
|
||||
// Send a GET request to /system/configuration to test copnnection
|
||||
let isValid = false;
|
||||
let errorMessage = "";
|
||||
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
await axios
|
||||
.get("http://localhost:3003/sync/writeSeasonsAndEpisodes")
|
||||
.get("/sync/writeLibraryItems")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
|
||||
await axios
|
||||
.get("/sync/writeSeasonsAndEpisodes")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
isValid = true;
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return { isValid: isValid, errorMessage: errorMessage };
|
||||
setProcessing(false);
|
||||
// return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
writeSeasonsAndEpisodes();
|
||||
console.log('Button clicked!');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Run Sync</button>
|
||||
<div className="settings-form">
|
||||
<button style={{backgroundColor: !processing? '#2196f3':'darkgrey',cursor: !processing? 'pointer':'default' }} disabled={processing} onClick={handleClick}>Run Sync</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function SettingsConfig() {
|
||||
|
||||
// Send a POST request to /api/setconfig/ with the updated configuration
|
||||
axios
|
||||
.post("http://localhost:3003/api/setconfig/", formValues, {
|
||||
.post("/api/setconfig/", formValues, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
127
src/pages/components/statCards/most_active_users.js
Normal file
127
src/pages/components/statCards/most_active_users.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
|
||||
function MostActiveUsers() {
|
||||
const [data, setData] = useState([]);
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostActiveUsers`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
|
||||
|
||||
const handleImageError = () => {
|
||||
setImgError(true);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
{imgError ?
|
||||
|
||||
<AccountCircleFillIcon size={'80%'}/>
|
||||
:
|
||||
|
||||
<img
|
||||
className="popular-user-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Users/" +
|
||||
(data[0].UserId) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
onError={handleImageError}
|
||||
alt=""
|
||||
></img>
|
||||
}
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST ACTIVE USERS</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.UserId}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.UserName}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MostActiveUsers;
|
||||
107
src/pages/components/statCards/most_used_client.js
Normal file
107
src/pages/components/statCards/most_used_client.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
import ComputerLineIcon from "remixicon-react/ComputerLineIcon";
|
||||
|
||||
function MostUsedClient() {
|
||||
const [data, setData] = useState([]);
|
||||
// const [base_url, setURL] = useState("");
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostUsedClient`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<div className="library-icons">
|
||||
<ComputerLineIcon size={'80%'}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST USED CLIENTS</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Client}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Client}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MostUsedClient;
|
||||
130
src/pages/components/statCards/mp_movies.js
Normal file
130
src/pages/components/statCards/mp_movies.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
function MPMovies(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostPopularMovies`;
|
||||
|
||||
axios
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&quality=50"
|
||||
})`}}
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<img
|
||||
className="popular-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST POPULAR MOVIES</div>
|
||||
<div className="stats-header-plays">Users</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.unique_viewers}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MPMovies;
|
||||
131
src/pages/components/statCards/mp_series.js
Normal file
131
src/pages/components/statCards/mp_series.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
function MPSeries(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
// const [base_url, setURL] = useState("");
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
console.log('PROPS: '+ days);
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostPopularSeries`;
|
||||
|
||||
axios
|
||||
.post(url, { days: props.days }, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&quality=50"
|
||||
})`}}
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<img
|
||||
className="popular-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST POPULAR SERIES</div>
|
||||
<div className="stats-header-plays">Users</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.unique_viewers}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MPSeries;
|
||||
121
src/pages/components/statCards/mv_libraries.js
Normal file
121
src/pages/components/statCards/mv_libraries.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
import TvLineIcon from "remixicon-react/TvLineIcon";
|
||||
import FilmLineIcon from "remixicon-react/FilmLineIcon";
|
||||
|
||||
function MVLibraries(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostViewedLibraries`;
|
||||
|
||||
axios
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<div className="library-icons">
|
||||
{data[0].CollectionType==="tvshows" ?
|
||||
|
||||
<TvLineIcon size={'80%'}/>
|
||||
:
|
||||
<FilmLineIcon size={'80%'}/>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST VIEWED LIBRARIES</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MVLibraries;
|
||||
130
src/pages/components/statCards/mv_movies.js
Normal file
130
src/pages/components/statCards/mv_movies.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
|
||||
function MVMovies(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostViewedMovies`;
|
||||
|
||||
axios
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&quality=50"
|
||||
})`}}
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<img
|
||||
className="popular-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST VIEWED MOVIES</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MVMovies;
|
||||
130
src/pages/components/statCards/mv_series.js
Normal file
130
src/pages/components/statCards/mv_series.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
|
||||
import ComponentLoading from "../ComponentLoading";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
function MVSeries(props) {
|
||||
const [data, setData] = useState([]);
|
||||
const [days, setDays] = useState(30);
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `/stats/getMostViewedSeries`;
|
||||
|
||||
axios
|
||||
.post(url, {days:props.days}, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data || data.length===0) {
|
||||
fetchLibraries();
|
||||
}
|
||||
if (days !== props.days) {
|
||||
setDays(props.days);
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, days,props.days]);
|
||||
|
||||
if (!data) {
|
||||
return(
|
||||
<div className="stats-card">
|
||||
<ComponentLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (data.length === 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="stats-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&quality=50"
|
||||
})`}}
|
||||
>
|
||||
|
||||
<div className="popular-image">
|
||||
<img
|
||||
className="popular-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
(data[0].Id) +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stats-header">
|
||||
|
||||
<div>MOST VIEWED SERIES</div>
|
||||
<div className="stats-header-plays">Plays</div>
|
||||
</div>
|
||||
|
||||
<div className = "stats-list">
|
||||
|
||||
{data &&
|
||||
data
|
||||
.map((item,index) => (
|
||||
|
||||
<div className='stat-item' key={item.Id}>
|
||||
<p className="stat-item-index">{(index+1)}</p>
|
||||
<p className="stat-item-name">{item.Name}</p>
|
||||
<p className="stat-item-count"> {item.Plays}</p>
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MVSeries;
|
||||
57
src/pages/css/activity.css
Normal file
57
src/pages/css/activity.css
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
.Activity
|
||||
{
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.Activity ul
|
||||
{
|
||||
list-style-type: none !important;
|
||||
color: white;
|
||||
width: fit-content;
|
||||
|
||||
}
|
||||
|
||||
.ActivityDetail
|
||||
{
|
||||
font-size: 1em;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.ActivityTime
|
||||
{
|
||||
/* text-align: right; */
|
||||
font-size: small;
|
||||
font-style: italic;
|
||||
color: lightgray;
|
||||
}
|
||||
ul{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li.old {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
animation-name: fade-out;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
li.new {
|
||||
|
||||
/* border: 2px solid grey; */
|
||||
border-radius: 5px;
|
||||
padding:10px 10px;
|
||||
margin:10px 10px 0px 0px;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
animation-name: fade-in;
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0s;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,113 @@
|
||||
.overview {
|
||||
color: white;
|
||||
margin-top: 20px;
|
||||
font-family: 'Railway', sans-serif;
|
||||
font-weight: bold;
|
||||
.overview-container
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(520px, 520px));
|
||||
grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/
|
||||
|
||||
border-radius: 4px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.library-card
|
||||
{
|
||||
width: 500px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: white;
|
||||
/* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.5); */
|
||||
background: linear-gradient(to right, #00A4DC, #AA5CC3);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.library-icons
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.library-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
.library-header-count {
|
||||
color: lightgray;
|
||||
font-weight: 300;
|
||||
|
||||
}
|
||||
|
||||
.library-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
}
|
||||
|
||||
.card {
|
||||
.library-item-index {
|
||||
padding-top: 3px;
|
||||
font-size: 0.8em;
|
||||
padding-right: 2px;
|
||||
color: grey;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
.library-item-name {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.library-item-count {
|
||||
width: 60%;
|
||||
text-align: right;
|
||||
color: #00A4DC;
|
||||
font-weight: 500;
|
||||
font-size: 1.1em;
|
||||
|
||||
}
|
||||
|
||||
.library-image
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 180px;
|
||||
width: 180px;
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.library-banner-image
|
||||
{
|
||||
|
||||
|
||||
height: 180px;
|
||||
width: 120px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.library-user-image
|
||||
{
|
||||
|
||||
border-radius: 50%;
|
||||
width: 80%;
|
||||
object-fit: cover;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.library{
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-right: 20px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-bottom: 5px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.item-card-count {
|
||||
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
padding: 5px 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.item-count {
|
||||
backdrop-filter: blur(2px);
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.item-count > div {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-count > div p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-count > div:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,16 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* background-color: rgba(255, 255, 255, 0.8); */
|
||||
z-index: 9999;
|
||||
}
|
||||
.component-loading {
|
||||
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(520px, 520px));
|
||||
grid-auto-rows: 235px;/* max-width+offset so 215 + 20*/
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
/* background-color: rgba(0,0,0,0.5); */
|
||||
/* padding: 20px; */
|
||||
/* border-radius: 4px; */
|
||||
margin-right: 20px;
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
color: white;
|
||||
|
||||
background-color: grey;
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.05);
|
||||
/* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */
|
||||
|
||||
max-height: 215px;
|
||||
max-width: 500px;
|
||||
@@ -24,7 +24,7 @@
|
||||
/* margin-bottom: 10px; */
|
||||
|
||||
background-size: cover;
|
||||
border-radius: 4px 4px 0px 4px;
|
||||
/* border-radius: 4px 4px 0px 4px; */
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
@@ -41,14 +41,14 @@
|
||||
grid-column: 1/3;
|
||||
height: 5px;
|
||||
background-color: #101010;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
/* border-radius: 0px 0px 4px 4px; */
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background-color: #00A4DC;
|
||||
transition: width 0.2s ease-in-out;
|
||||
border-radius: 0px 0px 0px 4px;
|
||||
/* border-radius: 0px 0px 0px 4px; */
|
||||
}
|
||||
|
||||
.card-banner {
|
||||
@@ -76,8 +76,9 @@
|
||||
|
||||
|
||||
.card-banner-image {
|
||||
border-radius: 4px 0px 0px 0px;
|
||||
/* border-radius: 4px 0px 0px 0px; */
|
||||
max-height: inherit;
|
||||
/* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */
|
||||
}
|
||||
|
||||
.card-user {
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease-in-out;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-form button:hover {
|
||||
|
||||
170
src/pages/css/statCard.css
Normal file
170
src/pages/css/statCard.css
Normal file
@@ -0,0 +1,170 @@
|
||||
.Heading
|
||||
{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Heading h1
|
||||
{
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.stat-cards-container
|
||||
{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(520px, 520px));
|
||||
grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/
|
||||
|
||||
border-radius: 4px;
|
||||
margin-right: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stats-card
|
||||
{
|
||||
width: 500px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
color: white;
|
||||
/* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.5); */
|
||||
background: linear-gradient(to right, #00A4DC, #AA5CC3);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.library-icons
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
.stats-header-plays {
|
||||
color: lightgray;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
}
|
||||
|
||||
.stat-item-index {
|
||||
padding-top: 6px;
|
||||
font-size: 0.8em;
|
||||
padding-right: 2px;
|
||||
color: grey;
|
||||
|
||||
text-align: right;
|
||||
}
|
||||
.stat-item-name {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.stat-item-count {
|
||||
width: 10%;
|
||||
text-align: right;
|
||||
color: #00A4DC;
|
||||
font-weight: 500;
|
||||
font-size: 1.1em;
|
||||
|
||||
}
|
||||
|
||||
.popular-image
|
||||
{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 180px;
|
||||
width: 175px;
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.popular-banner-image
|
||||
{
|
||||
|
||||
|
||||
height: 180px;
|
||||
width: 120px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.popular-user-image
|
||||
{
|
||||
|
||||
border-radius: 50%;
|
||||
width: 80%;
|
||||
object-fit: cover;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.stats{
|
||||
width: 100%;
|
||||
padding: 5px 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
background-color: rgb(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.date-range
|
||||
{
|
||||
width: 220px;
|
||||
height: 35px;
|
||||
color: white;
|
||||
display: flex;
|
||||
background-color: rgb(0, 99, 248,0.6);
|
||||
border-radius: 4px;
|
||||
font-size: 1.2em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
||||
.date-range .days input
|
||||
{
|
||||
height: 35px;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color:transparent;
|
||||
color:white;
|
||||
font-size: 1em;
|
||||
width: 40px;
|
||||
}
|
||||
.date-range .days
|
||||
{
|
||||
background-color: rgb(255, 255, 255, 0.1);
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
|
||||
input[type=number]::-webkit-outer-spin-button,
|
||||
input[type=number]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
|
||||
.date-range .header,
|
||||
.date-range .trailer
|
||||
{
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
font-family: sans-serif;
|
||||
min-width: 400px;
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.15);
|
||||
|
||||
color: white;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
@@ -22,7 +22,9 @@
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
/* text-align: left; */
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-left:1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
th {
|
||||
@@ -36,9 +38,9 @@ th:hover {
|
||||
|
||||
|
||||
|
||||
tbody tr:last-of-type {
|
||||
/* tbody tr:last-of-type {
|
||||
border-bottom: 2px solid #009879;
|
||||
}
|
||||
} */
|
||||
|
||||
|
||||
.card-user-image
|
||||
@@ -50,5 +52,16 @@ tbody tr:last-of-type {
|
||||
|
||||
}
|
||||
|
||||
tr:hover
|
||||
{
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
td:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ import React from 'react'
|
||||
import './css/home.css'
|
||||
|
||||
import Sessions from './components/sessions'
|
||||
import StatCards from './components/StatsCards'
|
||||
import LibraryOverView from './components/libraryOverview'
|
||||
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<LibraryOverView/>
|
||||
<h1>Sessions</h1>
|
||||
|
||||
<Sessions />
|
||||
<StatCards/>
|
||||
<LibraryOverView/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,13 +3,42 @@ import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import "./css/libraries.css";
|
||||
import "./css/usersactivity.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
// import PlaybackActivity from "./components/playbackactivity";
|
||||
|
||||
function Libraries() {
|
||||
const [data, setData] = useState([]);
|
||||
const [items, setItems] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
async function fetchLibraryData(libraryId) {
|
||||
console.log("data: "+libraryId);
|
||||
if (config) {
|
||||
const url = `/api/getLibraryItems`;
|
||||
await axios
|
||||
.post(url, {}, {
|
||||
headers: {
|
||||
"id": libraryId,
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("data");
|
||||
setItems(response.data);
|
||||
console.log(response);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
@@ -22,7 +51,7 @@ function Libraries() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
const fetchLibraries = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/Library/MediaFolders`;
|
||||
const apiKey = config.apiKey;
|
||||
@@ -43,22 +72,28 @@ function Libraries() {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 60);
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 60);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
const handleClick = (event) => {
|
||||
fetchLibraryData(event.target.value);
|
||||
console.log(event.target.value);
|
||||
// console.log('Button clicked!');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Activity">
|
||||
@@ -71,22 +106,42 @@ function Libraries() {
|
||||
)
|
||||
.map((item) => (
|
||||
<li key={item.Id}>
|
||||
{/* <div className='ActivityDetail'> {item.Name}</div> */}
|
||||
<div className="library-banner">
|
||||
<img
|
||||
className="library-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
item.Id +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
</div>
|
||||
<div className='ActivityDetail'> {item.Name}</div>
|
||||
<button onClick={handleClick} value= {item.Id}> {item.Name}</button>
|
||||
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<h1>Library Data</h1>
|
||||
<table className="user-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th >Id</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr key={item.Id}>
|
||||
<td>{item.Id}</td>
|
||||
<td>{item.Name}</td>
|
||||
<td>{item.Type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* <ul>
|
||||
{items &&
|
||||
items.map((item) => (
|
||||
<li key={item.Id}>
|
||||
<p className='ActivityDetail'> {item.Name}</p>
|
||||
<p className='ActivityDetail'> {item.Id}</p>
|
||||
|
||||
</li>
|
||||
))}
|
||||
</ul> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function Setup() {
|
||||
|
||||
// Send a POST request to /api/setconfig/ with the updated configuration
|
||||
axios
|
||||
.post("http://localhost:3003/api/setconfig/", formValues, {
|
||||
.post("/api/setconfig/", formValues, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -3,8 +3,15 @@ import React, { useState, useEffect } from 'react';
|
||||
import './css/libraries.css';
|
||||
import Loading from './components/loading';
|
||||
|
||||
// import PlaybackActivity from './components/playbackactivity';
|
||||
|
||||
// import StatCards from './components/StatsCards';
|
||||
|
||||
import LibraryOverView from './components/libraryOverview';
|
||||
|
||||
import API from '../classes/jellyfin-api';
|
||||
|
||||
|
||||
function UserData() {
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
@@ -26,15 +33,19 @@ function UserData() {
|
||||
|
||||
return (
|
||||
<div className='Activity'>
|
||||
<h1>Libraries</h1>
|
||||
{/* <h1>Libraries</h1>
|
||||
<ul>
|
||||
{data.map((series) => (
|
||||
<li key={series.Id}>
|
||||
<div className='ActivityDetail'>{series.Name}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ul> */}
|
||||
{/* <PlaybackActivity/> */}
|
||||
<LibraryOverView/>
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
26
src/setupProxy.js
Normal file
26
src/setupProxy.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(
|
||||
'/api',
|
||||
createProxyMiddleware({
|
||||
target: 'http://localhost:3003',
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
'/stats',
|
||||
createProxyMiddleware({
|
||||
target: 'http://localhost:3003',
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
'/sync',
|
||||
createProxyMiddleware({
|
||||
target: 'http://localhost:3003',
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
console.log('Proxy middleware applied to /api');
|
||||
};
|
||||
Reference in New Issue
Block a user