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:
Thegan Govender
2023-03-19 22:01:40 +02:00
parent 28ed76d6c4
commit 582a39918e
59 changed files with 2882 additions and 490 deletions

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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=';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};

View 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
};

View 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,
};

View 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,
};

View 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,
};

View 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,
};

View 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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: `);

View 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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}
*/

View File

@@ -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>
);
}

View 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;

View 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;

View File

@@ -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>
);
}

View 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;

View File

@@ -98,7 +98,7 @@ function sessionCard(props) {
return (
<div
key={props.data.session.Id}
className="session-card"
style={{
backgroundImage: `url(${

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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>
);

View File

@@ -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",
},

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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
View 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;
}

View File

@@ -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;
}

View File

@@ -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>
)
}

View File

@@ -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>
);
}

View File

@@ -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",
},

View File

@@ -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
View 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');
};