Changes for the daily & weekly playback duration charts

This commit is contained in:
Sanidhya Singh
2025-05-06 00:24:16 +05:30
parent c1800334a6
commit 579a7a3f8b
11 changed files with 319 additions and 43 deletions

View File

@@ -5,7 +5,7 @@ exports.up = async function (knex) {
CREATE OR REPLACE FUNCTION public.fs_watch_stats_over_time(
days integer)
RETURNS TABLE("Date" date, "Count" bigint, "TotalTime" bigint, "Library" text, "LibraryID" text)
RETURNS TABLE("Date" date, "Count" bigint, "Duration" bigint, "Library" text, "LibraryID" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
@@ -17,7 +17,7 @@ AS $BODY$
SELECT
dates."Date",
COALESCE(counts."Count", 0) AS "Count",
COALESCE(counts."TotalTime", 0) AS "TotalTime",
COALESCE(counts."Duration", 0) AS "Duration",
l."Name" as "Library",
l."Id" as "LibraryID"
FROM
@@ -32,7 +32,7 @@ AS $BODY$
(SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
(SUM(a."PlaybackDuration") / 60)::bigint AS "TotalTime",
(SUM(a."PlaybackDuration") / 60)::bigint AS "Duration",
l."Name" as "Library"
FROM
jf_playback_activity a

View File

@@ -0,0 +1,141 @@
exports.up = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_days_of_week(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_days_of_week(
days integer)
RETURNS TABLE("Day" text, "Count" bigint, "Duration" bigint, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
WITH library_days AS (
SELECT
l."Name" AS "Library",
d.day_of_week,
d.day_name
FROM
jf_libraries l,
(SELECT 0 AS "day_of_week", 'Sunday' AS "day_name" UNION ALL
SELECT 1 AS "day_of_week", 'Monday' AS "day_name" UNION ALL
SELECT 2 AS "day_of_week", 'Tuesday' AS "day_name" UNION ALL
SELECT 3 AS "day_of_week", 'Wednesday' AS "day_name" UNION ALL
SELECT 4 AS "day_of_week", 'Thursday' AS "day_name" UNION ALL
SELECT 5 AS "day_of_week", 'Friday' AS "day_name" UNION ALL
SELECT 6 AS "day_of_week", 'Saturday' AS "day_name"
) d
where l.archived=false
)
SELECT
library_days.day_name AS "Day",
COALESCE(SUM(counts."Count"), 0)::bigint AS "Count",
COALESCE(SUM(counts."Duration"), 0)::bigint AS "Duration",
library_days."Library" AS "Library"
FROM
library_days
LEFT JOIN
(SELECT
DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
COUNT(*) AS "Count",
(SUM(a."PlaybackDuration") / 60)::bigint AS "Duration",
EXTRACT(DOW FROM a."ActivityDateInserted") AS "DOW",
l."Name" AS "Library"
FROM
jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
JOIN jf_libraries l ON i."ParentId" = l."Id" and l.archived=false
WHERE
a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
GROUP BY
l."Name", EXTRACT(DOW FROM a."ActivityDateInserted"), DATE_TRUNC('day', a."ActivityDateInserted")
) counts
ON counts."DOW" = library_days.day_of_week AND counts."Library" = library_days."Library"
GROUP BY
library_days.day_name, library_days.day_of_week, library_days."Library"
ORDER BY
library_days.day_of_week, library_days."Library";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_popular_days_of_week(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};
exports.down = async function (knex) {
// try {
// await knex.schema.raw(`
// DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_days_of_week(integer);
// CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_days_of_week(
// days integer)
// RETURNS TABLE("Day" text, "Count" bigint, "Library" text)
// LANGUAGE 'plpgsql'
// COST 100
// VOLATILE PARALLEL UNSAFE
// ROWS 1000
// AS $BODY$
// BEGIN
// RETURN QUERY
// WITH library_days AS (
// SELECT
// l."Name" AS "Library",
// d.day_of_week,
// d.day_name
// FROM
// jf_libraries l,
// (SELECT 0 AS "day_of_week", 'Sunday' AS "day_name" UNION ALL
// SELECT 1 AS "day_of_week", 'Monday' AS "day_name" UNION ALL
// SELECT 2 AS "day_of_week", 'Tuesday' AS "day_name" UNION ALL
// SELECT 3 AS "day_of_week", 'Wednesday' AS "day_name" UNION ALL
// SELECT 4 AS "day_of_week", 'Thursday' AS "day_name" UNION ALL
// SELECT 5 AS "day_of_week", 'Friday' AS "day_name" UNION ALL
// SELECT 6 AS "day_of_week", 'Saturday' AS "day_name"
// ) d
// )
// SELECT
// library_days.day_name AS "Day",
// COALESCE(SUM(counts."Count"), 0)::bigint AS "Count",
// library_days."Library" AS "Library"
// FROM
// library_days
// LEFT JOIN
// (SELECT
// DATE_TRUNC('day', a."ActivityDateInserted")::DATE AS "Date",
// COUNT(*) AS "Count",
// EXTRACT(DOW FROM a."ActivityDateInserted") AS "DOW",
// l."Name" AS "Library"
// FROM
// jf_playback_activity a
// JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
// JOIN jf_libraries l ON i."ParentId" = l."Id"
// WHERE
// a."ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' as INTERVAL) AND NOW()
// GROUP BY
// l."Name", EXTRACT(DOW FROM a."ActivityDateInserted"), DATE_TRUNC('day', a."ActivityDateInserted")
// ) counts
// ON counts."DOW" = library_days.day_of_week AND counts."Library" = library_days."Library"
// GROUP BY
// library_days.day_name, library_days.day_of_week, library_days."Library"
// ORDER BY
// library_days.day_of_week, library_days."Library";
// END;
// $BODY$;
// ALTER FUNCTION fs_watch_stats_popular_days_of_week(integer)
// OWNER TO "${process.env.POSTGRES_ROLE}";`);
// } catch (error) {
// console.error(error);
// }
};

View File

@@ -0,0 +1,115 @@
exports.up = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_hour_of_day(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_hour_of_day(
days integer)
RETURNS TABLE("Hour" integer, "Count" integer, "Duration" integer, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
h."Hour",
COUNT(a."Id")::integer AS "Count",
COALESCE(SUM(a."PlaybackDuration") / 60, 0)::integer AS "Duration",
l."Name" AS "Library"
FROM
(
SELECT
generate_series(0, 23) AS "Hour"
) h
CROSS JOIN jf_libraries l
LEFT JOIN jf_library_items i ON i."ParentId" = l."Id"
LEFT JOIN (
SELECT
"NowPlayingItemId",
DATE_PART('hour', "ActivityDateInserted") AS "Hour",
"Id",
"PlaybackDuration"
FROM
jf_playback_activity
WHERE
"ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' AS INTERVAL) AND NOW()
) a ON a."NowPlayingItemId" = i."Id" AND a."Hour"::integer = h."Hour"
WHERE
l.archived=false
and l."Id" IN (SELECT "Id" FROM jf_libraries)
GROUP BY
h."Hour",
l."Name"
ORDER BY
l."Name",
h."Hour";
END;
$BODY$;
ALTER FUNCTION public.fs_watch_stats_popular_hour_of_day(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";
`);
} catch (error) {
console.error(error);
}
};
exports.down = async function (knex) {
try {
await knex.schema.raw(`
DROP FUNCTION IF EXISTS public.fs_watch_stats_popular_hour_of_day(integer);
CREATE OR REPLACE FUNCTION public.fs_watch_stats_popular_hour_of_day(
days integer)
RETURNS TABLE("Hour" integer, "Count" integer, "Library" text)
LANGUAGE 'plpgsql'
COST 100
VOLATILE PARALLEL UNSAFE
ROWS 1000
AS $BODY$
BEGIN
RETURN QUERY
SELECT
h."Hour",
COUNT(a."Id")::integer AS "Count",
l."Name" AS "Library"
FROM
(
SELECT
generate_series(0, 23) AS "Hour"
) h
CROSS JOIN jf_libraries l
LEFT JOIN jf_library_items i ON i."ParentId" = l."Id"
LEFT JOIN (
SELECT
"NowPlayingItemId",
DATE_PART('hour', "ActivityDateInserted") AS "Hour",
"Id"
FROM
jf_playback_activity
WHERE
"ActivityDateInserted" BETWEEN NOW() - CAST(days || ' days' AS INTERVAL) AND NOW()
) a ON a."NowPlayingItemId" = i."Id" AND a."Hour"::integer = h."Hour"
WHERE
l."Id" IN (SELECT "Id" FROM jf_libraries)
GROUP BY
h."Hour",
l."Name"
ORDER BY
l."Name",
h."Hour";
END;
$BODY$;
ALTER FUNCTION fs_watch_stats_popular_hour_of_day(integer)
OWNER TO "${process.env.POSTGRES_ROLE}";`);
} catch (error) {
console.error(error);
}
};

View File

@@ -423,7 +423,7 @@ router.get("/getViewsOverTime", async (req, res) => {
stats.forEach((item) => {
const library = item.Library;
const count = item.Count;
const watchTime = item.TotalTime;
const duration = item.Duration;
const date = new Date(item.Date).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
@@ -436,7 +436,7 @@ router.get("/getViewsOverTime", async (req, res) => {
};
}
reorganizedData[date] = { ...reorganizedData[date], [library]: { count, watchTime } };
reorganizedData[date] = { ...reorganizedData[date], [library]: { count, duration } };
});
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
res.send(finalData);
@@ -463,6 +463,7 @@ router.get("/getViewsByDays", async (req, res) => {
stats.forEach((item) => {
const library = item.Library;
const count = item.Count;
const duration = item.Duration;
const day = item.Day;
if (!reorganizedData[day]) {
@@ -471,7 +472,7 @@ router.get("/getViewsByDays", async (req, res) => {
};
}
reorganizedData[day] = { ...reorganizedData[day], [library]: count };
reorganizedData[day] = { ...reorganizedData[day], [library]: { count, duration } };
});
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
res.send(finalData);
@@ -498,6 +499,7 @@ router.get("/getViewsByHour", async (req, res) => {
stats.forEach((item) => {
const library = item.Library;
const count = item.Count;
const duration = item.Duration;
const hour = item.Hour;
if (!reorganizedData[hour]) {
@@ -506,7 +508,7 @@ router.get("/getViewsByHour", async (req, res) => {
};
}
reorganizedData[hour] = { ...reorganizedData[hour], [library]: count };
reorganizedData[hour] = { ...reorganizedData[hour], [library]: { count, duration } };
});
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
res.send(finalData);

View File

@@ -167,11 +167,11 @@
"STAT_PAGE": {
"STATISTICS": "Statistics",
"DAILY_PLAY_PER_LIBRARY": "Daily Play Count Per Library",
"DAILY_TIME_PER_LIBRARY": "Daily Watch Time Per Library",
"DAILY_DURATION_PER_LIBRARY": "Daily Play Duration Per Library",
"PLAY_COUNT_BY": "Play Count By",
"PLAY_TIME_BY": "Play Time By",
"COUNT_VIEW": "Total Count",
"TIME_VIEW": "Total Time"
"PLAY_DURATION_BY": "Play Duration By",
"COUNT_VIEW": "Count",
"DURATION_VIEW": "Duration"
},
"SETTINGS_PAGE": {
"SETTINGS": "Settings",

View File

@@ -10,7 +10,7 @@ function DailyPlayStats(props) {
const [stats, setStats] = useState();
const [libraries, setLibraries] = useState();
const [days, setDays] = useState(20);
const [viewName, setViewName] = useState("viewCount");
const [viewName, setViewName] = useState("count");
const token = localStorage.getItem("token");
@@ -52,14 +52,14 @@ function DailyPlayStats(props) {
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [stats,libraries, days, props.days, token]);
}, [stats,libraries, days, props.days, props.viewName, token]);
if (!stats) {
return <></>;
}
const titleKey = viewName === "count" ? "STAT_PAGE.DAILY_PLAY_PER_LIBRARY" : "STAT_PAGE.DAILY_TIME_PER_LIBRARY";
const titleKey = viewName === "count" ? "STAT_PAGE.DAILY_PLAY_PER_LIBRARY" : "STAT_PAGE.DAILY_DURATION_PER_LIBRARY";
if (stats.length === 0) {
return (
<div className="main-widget">

View File

@@ -9,6 +9,7 @@ function PlayStatsByDay(props) {
const [stats, setStats] = useState();
const [libraries, setLibraries] = useState();
const [days, setDays] = useState(20);
const [viewName, setViewName] = useState("count");
const token = localStorage.getItem("token");
useEffect(() => {
@@ -41,19 +42,24 @@ function PlayStatsByDay(props) {
setDays(props.days);
fetchLibraries();
}
if (props.viewName !== viewName) {
setViewName(props.viewName);
}
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [stats, libraries, days, props.days, token]);
}, [stats, libraries, days, props.days, props.viewName, token]);
if (!stats) {
return <></>;
}
const titleKey = viewName === "count" ? "STAT_PAGE.PLAY_COUNT_BY" : "STAT_PAGE.PLAY_DURATION_BY";
if (stats.length === 0) {
return (
<div className="statistics-widget small">
<h1><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h1><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
</div>
@@ -62,9 +68,9 @@ function PlayStatsByDay(props) {
return (
<div className="statistics-widget">
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<div className="graph small">
<Chart libraries={libraries} stats={stats} />
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
</div>
</div>
);

View File

@@ -8,6 +8,7 @@ function PlayStatsByHour(props) {
const [stats, setStats] = useState();
const [libraries, setLibraries] = useState();
const [days, setDays] = useState(20);
const [viewName, setViewName] = useState("count");
const token = localStorage.getItem("token");
useEffect(() => {
@@ -40,19 +41,23 @@ function PlayStatsByHour(props) {
setDays(props.days);
fetchLibraries();
}
if (props.viewName !== viewName) {
setViewName(props.viewName);
}
const intervalId = setInterval(fetchLibraries, 60000 * 5);
return () => clearInterval(intervalId);
}, [stats, libraries, days, props.days, token]);
}, [stats, libraries, days, props.days, props.viewName, token]);
if (!stats) {
return <></>;
}
const titleKey = viewName === "count" ? "STAT_PAGE.PLAY_COUNT_BY" : "STAT_PAGE.PLAY_DURATION_BY";
if (stats.length === 0) {
return (
<div className="statistics-widget small">
<h1><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h1><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
</div>
@@ -62,9 +67,9 @@ function PlayStatsByHour(props) {
return (
<div className="statistics-widget">
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
<div className="graph small">
<Chart libraries={libraries} stats={stats} />
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
</div>
</div>
);

View File

@@ -143,12 +143,3 @@ input[type="number"] {
.item-name :hover {
color: var(--secondary-color) !important;
}
.pill-wrapper {
color: white;
display: flex;
border-radius: 8px;
font-size: 1.2em;
align-self: flex-end;
justify-content: center;
}

View File

@@ -47,6 +47,14 @@
margin-bottom: 10px !important;
}
.stats-tab-nav {
background-color: var(--secondary-background-color);
display: flex;
border-radius: 8px;
align-self: flex-end;
justify-content: center;
}
.chart-canvas {
width: 100%;
height: 400px;

View File

@@ -51,14 +51,22 @@ function Statistics() {
<h1>
<Trans i18nKey={"STAT_PAGE.STATISTICS"} />
</h1>
<div className="pill-wrapper">
<div className="stats-tab-nav">
<Tabs
activeKey={activeTab}
onSelect={setTab}
variant="pills"
>
<Tab eventKey="tabCount" title={<Trans i18nKey="STAT_PAGE.COUNT_VIEW" />} />
<Tab eventKey="tabTime" title={<Trans i18nKey="STAT_PAGE.TIME_VIEW" />} />
<Tab
eventKey="tabCount"
className="bg-transparent"
title={<Trans i18nKey="STAT_PAGE.COUNT_VIEW" />}
/>
<Tab
eventKey="tabDuration"
className="bg-transparent"
title={<Trans i18nKey="STAT_PAGE.DURATION_VIEW" />}
/>
</Tabs>
</div>
<div className="date-range">
@@ -75,23 +83,23 @@ function Statistics() {
</div>
{activeTab === "tabCount" && (
<>
<div>
<DailyPlayStats days={days} viewName="count" />
<div className="statistics-graphs">
<PlayStatsByDay days={days} viewName="count" />
<PlayStatsByHour days={days} viewName="count" />
</div>
</>
</div>
)}
{activeTab === "tabTime" && (
<>
<DailyPlayStats days={days} viewName="watchTime" />
{activeTab === "tabDuration" && (
<div>
<DailyPlayStats days={days} viewName="duration" />
<div className="statistics-graphs">
<PlayStatsByDay days={days} viewName="watchTime" />
<PlayStatsByHour days={days} viewName="watchTime" />
<PlayStatsByDay days={days} viewName="duration" />
<PlayStatsByHour days={days} viewName="duration" />
</div>
</>
</div>
)}
</div>
);