From 579a7a3f8b476949ebb2b0ca7f0f50fbde27b64b Mon Sep 17 00:00:00 2001 From: Sanidhya Singh Date: Tue, 6 May 2025 00:24:16 +0530 Subject: [PATCH] Changes for the daily & weekly playback duration charts --- ...watch_stats_over_time_include_duration.js} | 6 +- ...s_popular_days_of_week_include_duration.js | 141 ++++++++++++++++++ ...ts_popular_hour_of_day_include_duration.js | 115 ++++++++++++++ backend/routes/stats.js | 10 +- public/locales/en-UK/translation.json | 8 +- .../statistics/daily-play-count.jsx | 8 +- .../statistics/play-stats-by-day.jsx | 14 +- .../statistics/play-stats-by-hour.jsx | 13 +- src/pages/css/statCard.css | 9 -- src/pages/css/stats.css | 8 + src/pages/statistics.jsx | 30 ++-- 11 files changed, 319 insertions(+), 43 deletions(-) rename backend/migrations/{095_fs_watch_stats_over_time_include_total_time.js => 095_fs_watch_stats_over_time_include_duration.js} (94%) create mode 100644 backend/migrations/096_fs_watch_stats_popular_days_of_week_include_duration.js create mode 100644 backend/migrations/097_fs_watch_stats_popular_hour_of_day_include_duration.js diff --git a/backend/migrations/095_fs_watch_stats_over_time_include_total_time.js b/backend/migrations/095_fs_watch_stats_over_time_include_duration.js similarity index 94% rename from backend/migrations/095_fs_watch_stats_over_time_include_total_time.js rename to backend/migrations/095_fs_watch_stats_over_time_include_duration.js index d8d29cf..e88fa04 100644 --- a/backend/migrations/095_fs_watch_stats_over_time_include_total_time.js +++ b/backend/migrations/095_fs_watch_stats_over_time_include_duration.js @@ -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 diff --git a/backend/migrations/096_fs_watch_stats_popular_days_of_week_include_duration.js b/backend/migrations/096_fs_watch_stats_popular_days_of_week_include_duration.js new file mode 100644 index 0000000..630f72b --- /dev/null +++ b/backend/migrations/096_fs_watch_stats_popular_days_of_week_include_duration.js @@ -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); +// } +}; diff --git a/backend/migrations/097_fs_watch_stats_popular_hour_of_day_include_duration.js b/backend/migrations/097_fs_watch_stats_popular_hour_of_day_include_duration.js new file mode 100644 index 0000000..4909788 --- /dev/null +++ b/backend/migrations/097_fs_watch_stats_popular_hour_of_day_include_duration.js @@ -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); + } +}; diff --git a/backend/routes/stats.js b/backend/routes/stats.js index 2cfe320..a3f105a 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -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); diff --git a/public/locales/en-UK/translation.json b/public/locales/en-UK/translation.json index 2d38e64..67d4b18 100644 --- a/public/locales/en-UK/translation.json +++ b/public/locales/en-UK/translation.json @@ -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", diff --git a/src/pages/components/statistics/daily-play-count.jsx b/src/pages/components/statistics/daily-play-count.jsx index a79f358..76411d6 100644 --- a/src/pages/components/statistics/daily-play-count.jsx +++ b/src/pages/components/statistics/daily-play-count.jsx @@ -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 (
diff --git a/src/pages/components/statistics/play-stats-by-day.jsx b/src/pages/components/statistics/play-stats-by-day.jsx index c7f5c09..7bc25cb 100644 --- a/src/pages/components/statistics/play-stats-by-day.jsx +++ b/src/pages/components/statistics/play-stats-by-day.jsx @@ -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 (
-

- {days} 1 ? 'S':''}`}/>

+

- {days} 1 ? 'S':''}`}/>

@@ -62,9 +68,9 @@ function PlayStatsByDay(props) { return (
-

- {days} 1 ? 'S':''}`}/>

+

- {days} 1 ? 'S':''}`}/>

- +
); diff --git a/src/pages/components/statistics/play-stats-by-hour.jsx b/src/pages/components/statistics/play-stats-by-hour.jsx index f1895d3..6198768 100644 --- a/src/pages/components/statistics/play-stats-by-hour.jsx +++ b/src/pages/components/statistics/play-stats-by-hour.jsx @@ -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 (
-

- {days} 1 ? 'S':''}`}/>

+

- {days} 1 ? 'S':''}`}/>

@@ -62,9 +67,9 @@ function PlayStatsByHour(props) { return (
-

- {days} 1 ? 'S':''}`}/>

+

- {days} 1 ? 'S':''}`}/>

- +
); diff --git a/src/pages/css/statCard.css b/src/pages/css/statCard.css index 2bf73ed..d4e2200 100644 --- a/src/pages/css/statCard.css +++ b/src/pages/css/statCard.css @@ -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; -} \ No newline at end of file diff --git a/src/pages/css/stats.css b/src/pages/css/stats.css index ca7761b..f300da5 100644 --- a/src/pages/css/stats.css +++ b/src/pages/css/stats.css @@ -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; diff --git a/src/pages/statistics.jsx b/src/pages/statistics.jsx index a81cb50..b04ff7e 100644 --- a/src/pages/statistics.jsx +++ b/src/pages/statistics.jsx @@ -51,14 +51,22 @@ function Statistics() {

-
+
- } /> - } /> + } + /> + } + />
@@ -75,23 +83,23 @@ function Statistics() {
{activeTab === "tabCount" && ( - <> +
- +
)} - {activeTab === "tabTime" && ( - <> - + {activeTab === "tabDuration" && ( +
+
- - + +
- +
)}
);