diff --git a/backend/classes/db-helper.js b/backend/classes/db-helper.js
new file mode 100644
index 0000000..b5f9976
--- /dev/null
+++ b/backend/classes/db-helper.js
@@ -0,0 +1,169 @@
+const { pool } = require("../db.js");
+const pgp = require("pg-promise")();
+
+function wrapField(field) {
+ if (field === "*") {
+ return field;
+ }
+ if (
+ field.includes("COALESCE") ||
+ field.includes("SUM") ||
+ field.includes("COUNT") ||
+ field.includes("MAX") ||
+ field.includes("MIN") ||
+ field.includes("AVG") ||
+ field.includes("DISTINCT") ||
+ field.includes("json_agg") ||
+ field.includes("CASE")
+ ) {
+ return field;
+ }
+ if (field.includes(" as ")) {
+ const [column, alias] = field.split(" as ");
+ return `${column
+ .split(".")
+ .map((part) => (part == "*" ? part : `"${part}"`))
+ .join(".")} as "${alias}"`;
+ }
+ return field
+ .split(".")
+ .map((part) => (part == "*" ? part : `"${part}"`))
+ .join(".");
+}
+
+function buildWhereClause(conditions) {
+ if (!Array.isArray(conditions)) {
+ return "";
+ }
+
+ return conditions
+ .map((condition, index) => {
+ if (Array.isArray(condition)) {
+ return `${index > 0 ? "AND" : ""} (${buildWhereClause(condition)})`;
+ } else if (typeof condition === "object") {
+ const { column, field, operator, value, type } = condition;
+ const conjunction = index === 0 ? "" : type ? type.toUpperCase() : "AND";
+ if (operator == "LIKE") {
+ return `${conjunction} ${column ? wrapField(column) : field} ${operator} ${value}`;
+ }
+ return `${conjunction} ${column ? wrapField(column) : field} ${operator} ${value}`;
+ }
+ return "";
+ })
+ .join(" ")
+ .trim();
+}
+
+function buildCTE(cte) {
+ if (!cte) {
+ return "";
+ }
+
+ const { select, table, cteAlias, alias, joins = [], where = [], group_by = [], order_by, sort_order = "desc" } = cte;
+ let query = `WITH ${cteAlias} AS (SELECT ${select.map(wrapField).join(", ")} FROM ${wrapField(table)} AS ${wrapField(alias)}`;
+
+ // Add joins
+ joins.forEach((join) => {
+ const joinConditions = join.conditions
+ .map((condition, index) => {
+ const conjunction = index === 0 ? "" : condition.type ? condition.type.toUpperCase() : "AND";
+ return `${conjunction} ${wrapField(condition.first)} ${condition.operator} ${
+ condition.second ? wrapField(condition.second) : `${condition.value}`
+ }`;
+ })
+ .join(" ");
+ const joinQuery = ` ${join.type.toUpperCase()} JOIN ${join.table} AS ${join.alias} ON ${joinConditions}`;
+ query += joinQuery;
+ });
+
+ // Add where conditions
+ const whereClause = buildWhereClause(where);
+ if (whereClause) {
+ query += ` WHERE ${whereClause}`;
+ }
+
+ // Add group by
+ if (group_by.length > 0) {
+ query += ` GROUP BY ${group_by.map(wrapField).join(", ")}`;
+ }
+
+ // Add order by
+ if (order_by) {
+ query += ` ORDER BY ${wrapField(order_by)} ${sort_order}`;
+ }
+
+ query += ")";
+ return query;
+}
+
+async function query({
+ cte,
+ select = ["*"],
+ table,
+ alias,
+ joins = [],
+ where = [],
+ values = [],
+ order_by = "Id",
+ sort_order = "desc",
+ pageNumber = 1,
+ pageSize = 50,
+}) {
+ const client = await pool.connect();
+ try {
+ // Build the base query
+ let countQuery = `${buildCTE(cte)} SELECT COUNT(*) FROM ${wrapField(table)} AS ${wrapField(alias)}`;
+ let query = `${buildCTE(cte)} SELECT ${select.map(wrapField).join(", ")} FROM ${wrapField(table)} AS ${wrapField(alias)}`;
+
+ // Add joins
+ joins.forEach((join) => {
+ const joinConditions = join.conditions
+ .map((condition, index) => {
+ const conjunction = index === 0 ? "" : condition.type ? condition.type.toUpperCase() : "AND";
+ return `${conjunction} ${wrapField(condition.first)} ${condition.operator} ${
+ condition.second ? wrapField(condition.second) : `${condition.value}`
+ }`;
+ })
+ .join(" ");
+ const joinQuery = ` ${join.type.toUpperCase()} JOIN ${join.table} AS ${join.alias} ON ${joinConditions}`;
+ query += joinQuery;
+ countQuery += joinQuery;
+ });
+
+ // Add where conditions
+ const whereClause = buildWhereClause(where);
+ if (whereClause) {
+ query += ` WHERE ${whereClause}`;
+ countQuery += ` WHERE ${whereClause}`;
+ }
+
+ // Add order by and pagination
+ query += ` ORDER BY ${wrapField(order_by)} ${sort_order}`;
+ query += ` LIMIT ${pageSize} OFFSET ${(pageNumber - 1) * pageSize}`;
+
+ // Execute the query
+ const result = await client.query(query, values);
+
+ // Count total rows
+ const countResult = await client.query(countQuery, values);
+ const totalRows = parseInt(countResult.rows[0].count, 10);
+
+ // Return the structured response
+ return {
+ pages: Math.ceil(totalRows / pageSize),
+ results: result.rows,
+ };
+ } catch (error) {
+ // console.timeEnd("queryWithPagingAndJoins");
+ console.error("Error occurred while executing query:", error.message);
+ return {
+ pages: 0,
+ results: [],
+ };
+ } finally {
+ client.release();
+ }
+}
+module.exports = {
+ query,
+};
diff --git a/backend/db.js b/backend/db.js
index 9eaa16c..748eb27 100644
--- a/backend/db.js
+++ b/backend/db.js
@@ -1,7 +1,6 @@
const { Pool } = require("pg");
const pgp = require("pg-promise")();
const { update_query: update_query_map } = require("./models/bulk_insert_update_handler");
-const moment = require("moment");
const _POSTGRES_USER = process.env.POSTGRES_USER;
const _POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD;
@@ -20,6 +19,9 @@ const pool = new Pool({
database: _POSTGRES_DATABASE,
password: _POSTGRES_PASSWORD,
port: _POSTGRES_PORT,
+ max: 20, // Maximum number of connections in the pool
+ idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
+ connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
});
pool.on("error", (err, client) => {
@@ -45,6 +47,12 @@ async function deleteBulk(table_name, data, pkName) {
await client.query("COMMIT");
message = data.length + " Rows removed.";
+
+ if (table_name === "jf_playback_activity") {
+ for (const view of materializedViews) {
+ refreshMaterializedView(view);
+ }
+ }
} catch (error) {
await client.query("ROLLBACK");
message = "Bulk delete error: " + error;
@@ -85,6 +93,32 @@ async function updateSingleFieldBulk(table_name, data, field_name, new_value, wh
return { Result: result, message: "" + message };
}
+const materializedViews = ["js_latest_playback_activity", "js_library_stats_overview"];
+
+async function refreshMaterializedView(view_name) {
+ const client = await pool.connect();
+ let result = "SUCCESS";
+ let message = "";
+ try {
+ await client.query("BEGIN");
+
+ const refreshQuery = {
+ text: `REFRESH MATERIALIZED VIEW ${view_name}`,
+ };
+ await client.query(refreshQuery);
+
+ await client.query("COMMIT");
+ message = view_name + " refreshed.";
+ } catch (error) {
+ await client.query("ROLLBACK");
+ message = "Refresh materialized view error: " + error;
+ result = "ERROR";
+ } finally {
+ client.release();
+ }
+ return { Result: result, message: "" + message };
+}
+
async function insertBulk(table_name, data, columns) {
//dedupe data
@@ -113,6 +147,12 @@ async function insertBulk(table_name, data, columns) {
const query = pgp.helpers.insert(data, cs) + update_query; // Update the column names accordingly
await client.query(query);
await client.query("COMMIT");
+
+ if (table_name === "jf_playback_activity") {
+ for (const view of materializedViews) {
+ refreshMaterializedView(view);
+ }
+ }
} catch (error) {
await client.query("ROLLBACK");
message = "" + error;
@@ -126,9 +166,15 @@ async function insertBulk(table_name, data, columns) {
};
}
-async function query(text, params) {
+async function query(text, params, refreshViews = false) {
try {
const result = await pool.query(text, params);
+
+ if (refreshViews) {
+ for (const view of materializedViews) {
+ refreshMaterializedView(view);
+ }
+ }
return result;
} catch (error) {
if (error?.routine === "auth_failed") {
@@ -163,6 +209,7 @@ async function querySingle(sql, params) {
}
module.exports = {
+ pool: pool,
query: query,
deleteBulk: deleteBulk,
insertBulk: insertBulk,
diff --git a/backend/migrations/079_create_view_jf_playback_activity_with_metadata.js b/backend/migrations/079_create_view_jf_playback_activity_with_metadata.js
new file mode 100644
index 0000000..31b539f
--- /dev/null
+++ b/backend/migrations/079_create_view_jf_playback_activity_with_metadata.js
@@ -0,0 +1,32 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP VIEW IF EXISTS public.jf_playback_activity_with_metadata;
+
+ CREATE VIEW jf_playback_activity_with_metadata AS
+ select a.*,e."IndexNumber" as "EpisodeNumber",e."ParentIndexNumber" as "SeasonNumber",i."ParentId"
+ FROM "jf_playback_activity" AS "a"
+ LEFT JOIN jf_library_episodes AS e
+ ON "a"."EpisodeId" = "e"."EpisodeId"
+ AND "a"."SeasonId" = "e"."SeasonId"
+ LEFT JOIN jf_library_items AS i
+ ON "i"."Id" = "a"."NowPlayingItemId" OR "e"."SeriesId" = "i"."Id"
+ order by a."ActivityDateInserted" desc;
+
+
+ ALTER VIEW public.jf_playback_activity_with_metadata
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP VIEW IF EXISTS public.jf_playback_activity_with_metadata;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/080_js_latest_playback_activity.js b/backend/migrations/080_js_latest_playback_activity.js
new file mode 100644
index 0000000..cc7e031
--- /dev/null
+++ b/backend/migrations/080_js_latest_playback_activity.js
@@ -0,0 +1,43 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_latest_playback_activity;
+
+ CREATE MATERIALIZED VIEW js_latest_playback_activity AS
+ WITH latest_activity AS (
+ SELECT
+ "NowPlayingItemId",
+ "EpisodeId",
+ "UserId",
+ MAX("ActivityDateInserted") AS max_date
+ FROM public.jf_playback_activity
+ GROUP BY "NowPlayingItemId", "EpisodeId", "UserId"
+ order by max_date desc
+ )
+ SELECT
+ a.*
+ FROM public.jf_playback_activity_with_metadata a
+ JOIN latest_activity u
+ ON a."NowPlayingItemId" = u."NowPlayingItemId"
+ AND COALESCE(a."EpisodeId", '1') = COALESCE(u."EpisodeId", '1')
+ AND a."UserId" = u."UserId"
+ AND a."ActivityDateInserted" = u.max_date
+ order by a."ActivityDateInserted" desc;
+
+
+ ALTER MATERIALIZED VIEW public.js_latest_playback_activity
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_latest_playback_activity;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/081_create_trigger_refresh_function_js_latest_playback_activity.js b/backend/migrations/081_create_trigger_refresh_function_js_latest_playback_activity.js
new file mode 100644
index 0000000..d0b4e10
--- /dev/null
+++ b/backend/migrations/081_create_trigger_refresh_function_js_latest_playback_activity.js
@@ -0,0 +1,28 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ CREATE OR REPLACE FUNCTION refresh_js_latest_playback_activity()
+ RETURNS TRIGGER AS $$
+ BEGIN
+ REFRESH MATERIALIZED VIEW js_latest_playback_activity;
+ RETURN NULL;
+ END;
+ $$ LANGUAGE plpgsql;
+
+
+ ALTER MATERIALIZED VIEW public.js_latest_playback_activity
+ 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.refresh_js_latest_playback_activity;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/082_create_trigger_refresh_js_latest_playback_activity.js b/backend/migrations/082_create_trigger_refresh_js_latest_playback_activity.js
new file mode 100644
index 0000000..767d4be
--- /dev/null
+++ b/backend/migrations/082_create_trigger_refresh_js_latest_playback_activity.js
@@ -0,0 +1,29 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP TRIGGER IF EXISTS refresh_js_latest_playback_activity_trigger ON public.jf_playback_activity;
+
+ DO $$
+ BEGIN
+ IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'refresh_js_latest_playback_activity') THEN
+ CREATE TRIGGER refresh_js_latest_playback_activity_trigger
+ AFTER INSERT OR UPDATE OR DELETE ON public.jf_playback_activity
+ FOR EACH STATEMENT
+ EXECUTE FUNCTION refresh_js_latest_playback_activity();
+ END IF;
+ END
+ $$ LANGUAGE plpgsql;
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP TRIGGER IF EXISTS refresh_js_latest_playback_activity_trigger ON public.jf_playback_activity;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/083_create_materialized_view_js_library_stats_overview.js b/backend/migrations/083_create_materialized_view_js_library_stats_overview.js
new file mode 100644
index 0000000..3b65269
--- /dev/null
+++ b/backend/migrations/083_create_materialized_view_js_library_stats_overview.js
@@ -0,0 +1,137 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP VIEW IF EXISTS public.js_library_stats_overview;
+ DROP MATERIALIZED VIEW IF EXISTS public.js_library_stats_overview;
+
+ CREATE MATERIALIZED VIEW public.js_library_stats_overview
+ AS
+ SELECT l."Id",
+ l."Name",
+ l."ServerId",
+ l."IsFolder",
+ l."Type",
+ l."CollectionType",
+ l."ImageTagsPrimary",
+ i."Id" AS "ItemId",
+ i."Name" AS "ItemName",
+ i."Type" AS "ItemType",
+ i."PrimaryImageHash",
+ s."IndexNumber" AS "SeasonNumber",
+ e."IndexNumber" AS "EpisodeNumber",
+ e."Name" AS "EpisodeName",
+ ( SELECT count(*) AS count
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS "Plays",
+ ( SELECT sum(a."PlaybackDuration") AS sum
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS total_playback_duration,
+ l.total_play_time::numeric AS total_play_time,
+ l.item_count AS "Library_Count",
+ l.season_count AS "Season_Count",
+ l.episode_count AS "Episode_Count",
+ l.archived,
+ now() - latest_activity."ActivityDateInserted" AS "LastActivity"
+ FROM jf_libraries l
+ LEFT JOIN (
+
+ SELECT
+ jso."Id",
+ jso."NowPlayingItemId",
+ jso."SeasonId",
+ jso."EpisodeId",
+ jso."ParentId",
+ jso."ActivityDateInserted"
+ FROM
+ jf_playback_activity_with_metadata jso
+ INNER JOIN (
+ SELECT
+ "ParentId",
+ MAX("ActivityDateInserted") AS max_date
+ FROM
+ jf_playback_activity_with_metadata
+ GROUP BY
+ "ParentId"
+ ) latest ON jso."ParentId" = latest."ParentId" AND jso."ActivityDateInserted" = latest.max_date
+
+ ) latest_activity ON l."Id" = latest_activity."ParentId"
+ LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
+ LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
+ LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
+ ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
+
+
+ ALTER MATERIALIZED VIEW public.js_library_stats_overview
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_library_stats_overview;
+ DROP VIEW IF EXISTS public.js_library_stats_overview;
+
+ CREATE OR REPLACE VIEW public.js_library_stats_overview
+ AS
+ SELECT DISTINCT ON (l."Id") l."Id",
+ l."Name",
+ l."ServerId",
+ l."IsFolder",
+ l."Type",
+ l."CollectionType",
+ l."ImageTagsPrimary",
+ i."Id" AS "ItemId",
+ i."Name" AS "ItemName",
+ i."Type" AS "ItemType",
+ i."PrimaryImageHash",
+ s."IndexNumber" AS "SeasonNumber",
+ e."IndexNumber" AS "EpisodeNumber",
+ e."Name" AS "EpisodeName",
+ ( SELECT count(*) AS count
+ FROM jf_playback_activity a
+ JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
+ WHERE i_1."ParentId" = l."Id") AS "Plays",
+ ( SELECT sum(a."PlaybackDuration") AS sum
+ FROM jf_playback_activity a
+ JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
+ WHERE i_1."ParentId" = l."Id") AS total_playback_duration,
+ l.total_play_time::numeric AS total_play_time,
+ l.item_count AS "Library_Count",
+ l.season_count AS "Season_Count",
+ l.episode_count AS "Episode_Count",
+ l.archived,
+ now() - latest_activity."ActivityDateInserted" AS "LastActivity"
+
+ FROM jf_libraries l
+ LEFT JOIN ( SELECT DISTINCT ON (i_1."ParentId") jf_playback_activity."Id",
+ jf_playback_activity."IsPaused",
+ jf_playback_activity."UserId",
+ jf_playback_activity."UserName",
+ jf_playback_activity."Client",
+ jf_playback_activity."DeviceName",
+ jf_playback_activity."DeviceId",
+ jf_playback_activity."ApplicationVersion",
+ jf_playback_activity."NowPlayingItemId",
+ jf_playback_activity."NowPlayingItemName",
+ jf_playback_activity."SeasonId",
+ jf_playback_activity."SeriesName",
+ jf_playback_activity."EpisodeId",
+ jf_playback_activity."PlaybackDuration",
+ jf_playback_activity."ActivityDateInserted",
+ jf_playback_activity."PlayMethod",
+ i_1."ParentId"
+ FROM jf_playback_activity
+ JOIN jf_library_items i_1 ON i_1."Id" = jf_playback_activity."NowPlayingItemId"
+ ORDER BY i_1."ParentId", jf_playback_activity."ActivityDateInserted" DESC) latest_activity ON l."Id" = latest_activity."ParentId"
+ LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
+ LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
+ LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
+ ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/084_create_trigger_refresh_function_js_library_stats_overview.js b/backend/migrations/084_create_trigger_refresh_function_js_library_stats_overview.js
new file mode 100644
index 0000000..04035bf
--- /dev/null
+++ b/backend/migrations/084_create_trigger_refresh_function_js_library_stats_overview.js
@@ -0,0 +1,28 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ CREATE OR REPLACE FUNCTION refresh_js_library_stats_overview()
+ RETURNS TRIGGER AS $$
+ BEGIN
+ REFRESH MATERIALIZED VIEW js_library_stats_overview;
+ RETURN NULL;
+ END;
+ $$ LANGUAGE plpgsql;
+
+
+ ALTER MATERIALIZED VIEW public.js_library_stats_overview
+ 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.refresh_js_library_stats_overview;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/085_create_trigger_refresh_js_library_stats_overview.js b/backend/migrations/085_create_trigger_refresh_js_library_stats_overview.js
new file mode 100644
index 0000000..c8a5f92
--- /dev/null
+++ b/backend/migrations/085_create_trigger_refresh_js_library_stats_overview.js
@@ -0,0 +1,29 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP TRIGGER IF EXISTS refresh_js_library_stats_overview_trigger ON public.jf_playback_activity;
+
+ DO $$
+ BEGIN
+ IF EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'refresh_js_library_stats_overview') THEN
+ CREATE TRIGGER refresh_js_library_stats_overview_trigger
+ AFTER INSERT OR UPDATE OR DELETE ON public.jf_playback_activity
+ FOR EACH STATEMENT
+ EXECUTE FUNCTION refresh_js_library_stats_overview();
+ END IF;
+ END
+ $$ LANGUAGE plpgsql;
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP TRIGGER IF EXISTS refresh_js_library_stats_overview_trigger ON public.jf_playback_activity;`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/086_drop_all_refresh_triggers_on_activity_table.js b/backend/migrations/086_drop_all_refresh_triggers_on_activity_table.js
new file mode 100644
index 0000000..a6df304
--- /dev/null
+++ b/backend/migrations/086_drop_all_refresh_triggers_on_activity_table.js
@@ -0,0 +1,21 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP TRIGGER IF EXISTS refresh_js_library_stats_overview_trigger ON public.jf_playback_activity;
+ DROP TRIGGER IF EXISTS refresh_js_latest_playback_activity_trigger ON public.jf_playback_activity;
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP TRIGGER IF EXISTS refresh_js_library_stats_overview_trigger ON public.jf_playback_activity;
+ DROP TRIGGER IF EXISTS refresh_js_latest_playback_activity_trigger ON public.jf_playback_activity;
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/087_optimize_js_latest_playback_activity.js b/backend/migrations/087_optimize_js_latest_playback_activity.js
new file mode 100644
index 0000000..82b3f4b
--- /dev/null
+++ b/backend/migrations/087_optimize_js_latest_playback_activity.js
@@ -0,0 +1,114 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_latest_playback_activity;
+
+ CREATE MATERIALIZED VIEW js_latest_playback_activity AS
+ WITH ranked_activity AS (
+ SELECT
+ "Id",
+ "IsPaused",
+ "UserId",
+ "UserName",
+ "Client",
+ "DeviceName",
+ "DeviceId",
+ "ApplicationVersion",
+ "NowPlayingItemId",
+ "NowPlayingItemName",
+ "SeasonId",
+ "SeriesName",
+ "EpisodeId",
+ "PlaybackDuration",
+ "ActivityDateInserted",
+ "PlayMethod",
+ "MediaStreams",
+ "TranscodingInfo",
+ "PlayState",
+ "OriginalContainer",
+ "RemoteEndPoint",
+ "ServerId",
+ imported,
+ "EpisodeNumber",
+ "SeasonNumber",
+ "ParentId",
+ ROW_NUMBER() OVER (
+ PARTITION BY "NowPlayingItemId", COALESCE("EpisodeId", '1'), "UserId"
+ ORDER BY "ActivityDateInserted" DESC
+ ) AS rn
+ FROM jf_playback_activity_with_metadata
+ )
+ SELECT
+ "Id",
+ "IsPaused",
+ "UserId",
+ "UserName",
+ "Client",
+ "DeviceName",
+ "DeviceId",
+ "ApplicationVersion",
+ "NowPlayingItemId",
+ "NowPlayingItemName",
+ "SeasonId",
+ "SeriesName",
+ "EpisodeId",
+ "PlaybackDuration",
+ "ActivityDateInserted",
+ "PlayMethod",
+ "MediaStreams",
+ "TranscodingInfo",
+ "PlayState",
+ "OriginalContainer",
+ "RemoteEndPoint",
+ "ServerId",
+ imported,
+ "EpisodeNumber",
+ "SeasonNumber",
+ "ParentId"
+ FROM ranked_activity
+ WHERE rn = 1
+ ORDER BY "ActivityDateInserted" DESC;
+
+
+ ALTER MATERIALIZED VIEW public.js_latest_playback_activity
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_latest_playback_activity;
+
+ CREATE MATERIALIZED VIEW js_latest_playback_activity AS
+ WITH latest_activity AS (
+ SELECT
+ "NowPlayingItemId",
+ "EpisodeId",
+ "UserId",
+ MAX("ActivityDateInserted") AS max_date
+ FROM public.jf_playback_activity
+ GROUP BY "NowPlayingItemId", "EpisodeId", "UserId"
+ order by max_date desc
+ )
+ SELECT
+ a.*
+ FROM public.jf_playback_activity_with_metadata a
+ JOIN latest_activity u
+ ON a."NowPlayingItemId" = u."NowPlayingItemId"
+ AND COALESCE(a."EpisodeId", '1') = COALESCE(u."EpisodeId", '1')
+ AND a."UserId" = u."UserId"
+ AND a."ActivityDateInserted" = u.max_date
+ order by a."ActivityDateInserted" desc;
+
+
+ ALTER MATERIALIZED VIEW public.js_latest_playback_activity
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/088_optimize_materialized_view_js_library_stats_overview.js b/backend/migrations/088_optimize_materialized_view_js_library_stats_overview.js
new file mode 100644
index 0000000..7cdbbf2
--- /dev/null
+++ b/backend/migrations/088_optimize_materialized_view_js_library_stats_overview.js
@@ -0,0 +1,130 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_library_stats_overview;
+
+ CREATE MATERIALIZED VIEW public.js_library_stats_overview
+ AS
+ SELECT l."Id",
+ l."Name",
+ l."ServerId",
+ l."IsFolder",
+ l."Type",
+ l."CollectionType",
+ l."ImageTagsPrimary",
+ i."Id" AS "ItemId",
+ i."Name" AS "ItemName",
+ i."Type" AS "ItemType",
+ i."PrimaryImageHash",
+ s."IndexNumber" AS "SeasonNumber",
+ e."IndexNumber" AS "EpisodeNumber",
+ e."Name" AS "EpisodeName",
+ ( SELECT count(*) AS count
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS "Plays",
+ ( SELECT sum(a."PlaybackDuration") AS sum
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS total_playback_duration,
+ l.total_play_time::numeric AS total_play_time,
+ l.item_count AS "Library_Count",
+ l.season_count AS "Season_Count",
+ l.episode_count AS "Episode_Count",
+ l.archived,
+ now() - latest_activity."ActivityDateInserted" AS "LastActivity"
+ FROM jf_libraries l
+ LEFT JOIN ( SELECT jso."Id",
+ jso."NowPlayingItemId",
+ jso."SeasonId",
+ jso."EpisodeId",
+ jso."ParentId",
+ jso."ActivityDateInserted"
+ FROM js_latest_playback_activity jso
+ JOIN ( SELECT js_latest_playback_activity."ParentId",
+ max(js_latest_playback_activity."ActivityDateInserted") AS max_date
+ FROM js_latest_playback_activity
+ GROUP BY js_latest_playback_activity."ParentId") latest
+ ON jso."ParentId" = latest."ParentId" AND jso."ActivityDateInserted" = latest.max_date )
+ latest_activity ON l."Id" = latest_activity."ParentId"
+
+ LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
+ LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
+ LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
+ ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
+
+
+ ALTER MATERIALIZED VIEW public.js_library_stats_overview
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_library_stats_overview;
+
+ CREATE MATERIALIZED VIEW public.js_library_stats_overview
+ AS
+ SELECT l."Id",
+ l."Name",
+ l."ServerId",
+ l."IsFolder",
+ l."Type",
+ l."CollectionType",
+ l."ImageTagsPrimary",
+ i."Id" AS "ItemId",
+ i."Name" AS "ItemName",
+ i."Type" AS "ItemType",
+ i."PrimaryImageHash",
+ s."IndexNumber" AS "SeasonNumber",
+ e."IndexNumber" AS "EpisodeNumber",
+ e."Name" AS "EpisodeName",
+ ( SELECT count(*) AS count
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS "Plays",
+ ( SELECT sum(a."PlaybackDuration") AS sum
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS total_playback_duration,
+ l.total_play_time::numeric AS total_play_time,
+ l.item_count AS "Library_Count",
+ l.season_count AS "Season_Count",
+ l.episode_count AS "Episode_Count",
+ l.archived,
+ now() - latest_activity."ActivityDateInserted" AS "LastActivity"
+ FROM jf_libraries l
+ LEFT JOIN (
+
+ SELECT
+ jso."Id",
+ jso."NowPlayingItemId",
+ jso."SeasonId",
+ jso."EpisodeId",
+ jso."ParentId",
+ jso."ActivityDateInserted"
+ FROM
+ jf_playback_activity_with_metadata jso
+ INNER JOIN (
+ SELECT
+ "ParentId",
+ MAX("ActivityDateInserted") AS max_date
+ FROM
+ jf_playback_activity_with_metadata
+ GROUP BY
+ "ParentId"
+ ) latest ON jso."ParentId" = latest."ParentId" AND jso."ActivityDateInserted" = latest.max_date
+
+ ) latest_activity ON l."Id" = latest_activity."ParentId"
+ LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
+ LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
+ LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
+ ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
+
+
+ ALTER MATERIALIZED VIEW public.js_library_stats_overview
+ OWNER TO "${process.env.POSTGRES_ROLE}";`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/089_optimize_materialized_view_js_library_stats_overview_last_activity.js b/backend/migrations/089_optimize_materialized_view_js_library_stats_overview_last_activity.js
new file mode 100644
index 0000000..721b1d2
--- /dev/null
+++ b/backend/migrations/089_optimize_materialized_view_js_library_stats_overview_last_activity.js
@@ -0,0 +1,122 @@
+exports.up = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_library_stats_overview;
+
+ CREATE MATERIALIZED VIEW public.js_library_stats_overview
+ AS
+ SELECT l."Id",
+ l."Name",
+ l."ServerId",
+ l."IsFolder",
+ l."Type",
+ l."CollectionType",
+ l."ImageTagsPrimary",
+ i."Id" AS "ItemId",
+ i."Name" AS "ItemName",
+ i."Type" AS "ItemType",
+ i."PrimaryImageHash",
+ s."IndexNumber" AS "SeasonNumber",
+ e."IndexNumber" AS "EpisodeNumber",
+ e."Name" AS "EpisodeName",
+ ( SELECT count(*) AS count
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS "Plays",
+ ( SELECT sum(a."PlaybackDuration") AS sum
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS total_playback_duration,
+ l.total_play_time::numeric AS total_play_time,
+ l.item_count AS "Library_Count",
+ l.season_count AS "Season_Count",
+ l.episode_count AS "Episode_Count",
+ l.archived,
+ latest_activity."ActivityDateInserted"
+ FROM jf_libraries l
+ LEFT JOIN ( SELECT jso."Id",
+ jso."NowPlayingItemId",
+ jso."SeasonId",
+ jso."EpisodeId",
+ jso."ParentId",
+ jso."ActivityDateInserted"
+ FROM js_latest_playback_activity jso
+ JOIN ( SELECT js_latest_playback_activity."ParentId",
+ max(js_latest_playback_activity."ActivityDateInserted") AS max_date
+ FROM js_latest_playback_activity
+ GROUP BY js_latest_playback_activity."ParentId") latest
+ ON jso."ParentId" = latest."ParentId" AND jso."ActivityDateInserted" = latest.max_date )
+ latest_activity ON l."Id" = latest_activity."ParentId"
+
+ LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
+ LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
+ LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
+ ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
+
+
+ ALTER MATERIALIZED VIEW public.js_library_stats_overview
+ OWNER TO "${process.env.POSTGRES_ROLE}";
+ `);
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+exports.down = async function (knex) {
+ try {
+ await knex.schema.raw(`
+ DROP MATERIALIZED VIEW IF EXISTS public.js_library_stats_overview;
+
+ CREATE MATERIALIZED VIEW public.js_library_stats_overview
+ AS
+ SELECT l."Id",
+ l."Name",
+ l."ServerId",
+ l."IsFolder",
+ l."Type",
+ l."CollectionType",
+ l."ImageTagsPrimary",
+ i."Id" AS "ItemId",
+ i."Name" AS "ItemName",
+ i."Type" AS "ItemType",
+ i."PrimaryImageHash",
+ s."IndexNumber" AS "SeasonNumber",
+ e."IndexNumber" AS "EpisodeNumber",
+ e."Name" AS "EpisodeName",
+ ( SELECT count(*) AS count
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS "Plays",
+ ( SELECT sum(a."PlaybackDuration") AS sum
+ FROM jf_playback_activity_with_metadata a
+ WHERE a."ParentId" = l."Id") AS total_playback_duration,
+ l.total_play_time::numeric AS total_play_time,
+ l.item_count AS "Library_Count",
+ l.season_count AS "Season_Count",
+ l.episode_count AS "Episode_Count",
+ l.archived,
+ now() - latest_activity."ActivityDateInserted" AS "LastActivity"
+ FROM jf_libraries l
+ LEFT JOIN ( SELECT jso."Id",
+ jso."NowPlayingItemId",
+ jso."SeasonId",
+ jso."EpisodeId",
+ jso."ParentId",
+ jso."ActivityDateInserted"
+ FROM js_latest_playback_activity jso
+ JOIN ( SELECT js_latest_playback_activity."ParentId",
+ max(js_latest_playback_activity."ActivityDateInserted") AS max_date
+ FROM js_latest_playback_activity
+ GROUP BY js_latest_playback_activity."ParentId") latest
+ ON jso."ParentId" = latest."ParentId" AND jso."ActivityDateInserted" = latest.max_date )
+ latest_activity ON l."Id" = latest_activity."ParentId"
+
+ LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
+ LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
+ LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
+ ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
+
+
+ ALTER MATERIALIZED VIEW public.js_library_stats_overview
+ OWNER TO "${process.env.POSTGRES_ROLE}";`);
+ } catch (error) {
+ console.error(error);
+ }
+};
diff --git a/backend/migrations/090_create_function_fs_get_user_activity.js b/backend/migrations/090_create_function_fs_get_user_activity.js
new file mode 100644
index 0000000..acf59c3
--- /dev/null
+++ b/backend/migrations/090_create_function_fs_get_user_activity.js
@@ -0,0 +1,73 @@
+exports.up = function (knex) {
+ return knex.schema.raw(`
+ create or replace
+ function fs_get_user_activity(
+ user_id text,
+ library_ids text[]
+ )
+ returns table (
+ "UserName" text,
+ "Title" text,
+ "EpisodeCount" bigint,
+ "FirstActivityDate" timestamptz,
+ "LastActivityDate" timestamptz,
+ "TotalPlaybackDuration" bigint,
+ "SeasonName" text,
+ "MediaType" text,
+ "NowPlayingItemId" text
+ ) as $$
+ begin
+ return QUERY
+ select
+ jp."UserName",
+ case
+ when jp."SeriesName" is not null then jp."SeriesName"
+ else jp."NowPlayingItemName"
+ end as "Title",
+ COUNT(distinct jp."EpisodeId") as "EpisodeCount",
+ MIN(jp."ActivityDateInserted") as "FirstActivityDate",
+ MAX(jp."ActivityDateInserted") as "LastActivityDate",
+ SUM(jp."PlaybackDuration")::bigint as "TotalPlaybackDuration",
+ ls."Name" as "SeasonName",
+ MAX(jl."CollectionType") as "MediaType",
+ jp."NowPlayingItemId"
+ from
+ public.jf_playback_activity as jp
+ join
+ public.jf_library_items as jli on
+ jp."NowPlayingItemId" = jli."Id"
+ join
+ public.jf_libraries as jl on
+ jli."ParentId" = jl."Id"
+ left join
+ public.jf_library_seasons as ls on
+ jp."SeasonId" = ls."Id"
+ where
+ jp."UserId" = user_id
+ and jl."Id" = any(library_ids)
+ group by
+ jp."UserName",
+ case
+ when jp."SeriesName" is not null then jp."SeriesName"
+ else jp."NowPlayingItemName"
+ end,
+ jp."SeriesName",
+ jp."SeasonId",
+ ls."Name",
+ jp."NowPlayingItemId"
+ having
+ not (MAX(jl."Name") = 'Shows'
+ and ls."Name" is null)
+ and SUM(jp."PlaybackDuration") >= 30
+ order by
+ MAX(jp."ActivityDateInserted") desc;
+ end;
+ $$ language plpgsql;
+`);
+};
+
+exports.down = function (knex) {
+ return knex.schema.raw(`
+ DROP FUNCTION IF EXISTS fs_get_user_activity;
+ `);
+};
diff --git a/backend/migrations/091_fix_function_fs_get_user_activity.js b/backend/migrations/091_fix_function_fs_get_user_activity.js
new file mode 100644
index 0000000..23d6f12
--- /dev/null
+++ b/backend/migrations/091_fix_function_fs_get_user_activity.js
@@ -0,0 +1,177 @@
+exports.up = function (knex) {
+ return knex.schema.raw(`
+ DROP FUNCTION IF EXISTS fs_get_user_activity;
+
+ CREATE OR REPLACE FUNCTION fs_get_user_activity(
+ user_id TEXT,
+ library_ids TEXT[]
+ )
+ RETURNS TABLE (
+ "UserName" TEXT,
+ "Title" TEXT,
+ "EpisodeCount" BIGINT,
+ "FirstActivityDate" TIMESTAMPTZ,
+ "LastActivityDate" TIMESTAMPTZ,
+ "TotalPlaybackDuration" BIGINT,
+ "SeasonName" TEXT,
+ "MediaType" TEXT,
+ "NowPlayingItemId" TEXT
+ ) AS $$
+ BEGIN
+ RETURN QUERY
+ WITH DateDifferences AS (
+ SELECT
+ jp."UserName" AS "UserNameCol",
+ CASE
+ WHEN jp."SeriesName" IS NOT NULL THEN jp."SeriesName"
+ ELSE jp."NowPlayingItemName"
+ END AS "TitleCol",
+ jp."EpisodeId" AS "EpisodeIdCol",
+ jp."ActivityDateInserted" AS "ActivityDateInsertedCol",
+ jp."PlaybackDuration" AS "PlaybackDurationCol",
+ ls."Name" AS "SeasonNameCol",
+ jl."CollectionType" AS "MediaTypeCol",
+ jp."NowPlayingItemId" AS "NowPlayingItemIdCol",
+ LAG(jp."ActivityDateInserted") OVER (PARTITION BY jp."UserName", CASE WHEN jp."SeriesName" IS NOT NULL THEN jp."SeriesName" ELSE jp."NowPlayingItemName" END ORDER BY jp."ActivityDateInserted") AS prev_date
+ FROM
+ public.jf_playback_activity AS jp
+ JOIN
+ public.jf_library_items AS jli ON jp."NowPlayingItemId" = jli."Id"
+ JOIN
+ public.jf_libraries AS jl ON jli."ParentId" = jl."Id"
+ LEFT JOIN
+ public.jf_library_seasons AS ls ON jp."SeasonId" = ls."Id"
+ WHERE
+ jp."UserId" = user_id
+ AND jl."Id" = ANY(library_ids)
+ ),
+ GroupedEntries AS (
+ SELECT
+ "UserNameCol",
+ "TitleCol",
+ "EpisodeIdCol",
+ "ActivityDateInsertedCol",
+ "PlaybackDurationCol",
+ "SeasonNameCol",
+ "MediaTypeCol",
+ "NowPlayingItemIdCol",
+ prev_date,
+ CASE
+ WHEN prev_date IS NULL OR "ActivityDateInsertedCol" > prev_date + INTERVAL '1 month' THEN 1 -- Pick whatever interval you want here, I'm biased as I don't monitor music / never intended this feature to be used for music
+ ELSE 0
+ END AS new_group
+ FROM
+ DateDifferences
+ ),
+ FinalGroups AS (
+ SELECT
+ "UserNameCol",
+ "TitleCol",
+ "EpisodeIdCol",
+ "ActivityDateInsertedCol",
+ "PlaybackDurationCol",
+ "SeasonNameCol",
+ "MediaTypeCol",
+ "NowPlayingItemIdCol",
+ SUM(new_group) OVER (PARTITION BY "UserNameCol", "TitleCol" ORDER BY "ActivityDateInsertedCol") AS grp
+ FROM
+ GroupedEntries
+ )
+ SELECT
+ "UserNameCol" AS "UserName",
+ "TitleCol" AS "Title",
+ COUNT(DISTINCT "EpisodeIdCol") AS "EpisodeCount",
+ MIN("ActivityDateInsertedCol") AS "FirstActivityDate",
+ MAX("ActivityDateInsertedCol") AS "LastActivityDate",
+ SUM("PlaybackDurationCol")::bigint AS "TotalPlaybackDuration",
+ "SeasonNameCol" AS "SeasonName",
+ MAX("MediaTypeCol") AS "MediaType",
+ "NowPlayingItemIdCol" AS "NowPlayingItemId"
+ FROM
+ FinalGroups
+ GROUP BY
+ "UserNameCol",
+ "TitleCol",
+ "SeasonNameCol",
+ "NowPlayingItemIdCol",
+ grp
+ HAVING
+ NOT (MAX("MediaTypeCol") = 'Shows' AND "SeasonNameCol" IS NULL)
+ AND SUM("PlaybackDurationCol") >= 20
+ ORDER BY
+ MAX("ActivityDateInsertedCol") DESC;
+ END;
+ $$ LANGUAGE plpgsql;
+
+`);
+};
+
+exports.down = function (knex) {
+ return knex.schema.raw(`
+
+ DROP FUNCTION IF EXISTS fs_get_user_activity;
+ create or replace
+ function fs_get_user_activity(
+ user_id text,
+ library_ids text[]
+ )
+ returns table (
+ "UserName" text,
+ "Title" text,
+ "EpisodeCount" bigint,
+ "FirstActivityDate" timestamptz,
+ "LastActivityDate" timestamptz,
+ "TotalPlaybackDuration" bigint,
+ "SeasonName" text,
+ "MediaType" text,
+ "NowPlayingItemId" text
+ ) as $$
+ begin
+ return QUERY
+ select
+ jp."UserName",
+ case
+ when jp."SeriesName" is not null then jp."SeriesName"
+ else jp."NowPlayingItemName"
+ end as "Title",
+ COUNT(distinct jp."EpisodeId") as "EpisodeCount",
+ MIN(jp."ActivityDateInserted") as "FirstActivityDate",
+ MAX(jp."ActivityDateInserted") as "LastActivityDate",
+ SUM(jp."PlaybackDuration")::bigint as "TotalPlaybackDuration",
+ ls."Name" as "SeasonName",
+ MAX(jl."CollectionType") as "MediaType",
+ jp."NowPlayingItemId"
+ from
+ public.jf_playback_activity as jp
+ join
+ public.jf_library_items as jli on
+ jp."NowPlayingItemId" = jli."Id"
+ join
+ public.jf_libraries as jl on
+ jli."ParentId" = jl."Id"
+ left join
+ public.jf_library_seasons as ls on
+ jp."SeasonId" = ls."Id"
+ where
+ jp."UserId" = user_id
+ and jl."Id" = any(library_ids)
+ group by
+ jp."UserName",
+ case
+ when jp."SeriesName" is not null then jp."SeriesName"
+ else jp."NowPlayingItemName"
+ end,
+ jp."SeriesName",
+ jp."SeasonId",
+ ls."Name",
+ jp."NowPlayingItemId"
+ having
+ not (MAX(jl."Name") = 'Shows'
+ and ls."Name" is null)
+ and SUM(jp."PlaybackDuration") >= 30
+ order by
+ MAX(jp."ActivityDateInserted") desc;
+ end;
+ $$ language plpgsql;
+ `);
+};
diff --git a/backend/routes/api.js b/backend/routes/api.js
index 3526802..f6d8adb 100644
--- a/backend/routes/api.js
+++ b/backend/routes/api.js
@@ -2,6 +2,8 @@
const express = require("express");
const db = require("../db");
+const dbHelper = require("../classes/db-helper");
+
const pgp = require("pg-promise")();
const { randomUUID } = require("crypto");
@@ -15,38 +17,48 @@ const { tables } = require("../global/backup_tables");
const router = express.Router();
+//consts
+const groupedSortMap = [
+ { field: "UserName", column: "a.UserName" },
+ { field: "RemoteEndPoint", column: "a.RemoteEndPoint" },
+ { field: "NowPlayingItemName", column: "FullName" },
+ { field: "Client", column: "a.Client" },
+ { field: "DeviceName", column: "a.DeviceName" },
+ { field: "ActivityDateInserted", column: "a.ActivityDateInserted" },
+ { field: "PlaybackDuration", column: `COALESCE(ar."TotalDuration", a."PlaybackDuration")` },
+ { field: "TotalPlays", column: `COALESCE("TotalPlays",1)` },
+];
+
+const unGroupedSortMap = [
+ { field: "UserName", column: "a.UserName" },
+ { field: "RemoteEndPoint", column: "a.RemoteEndPoint" },
+ { field: "NowPlayingItemName", column: "FullName" },
+ { field: "Client", column: "a.Client" },
+ { field: "DeviceName", column: "a.DeviceName" },
+ { field: "ActivityDateInserted", column: "a.ActivityDateInserted" },
+ { field: "PlaybackDuration", column: "a.PlaybackDuration" },
+];
+
+const filterFields = [
+ { field: "UserName", column: `LOWER(a."UserName")` },
+ { field: "RemoteEndPoint", column: `LOWER(a."RemoteEndPoint")` },
+ {
+ field: "NowPlayingItemName",
+ column: `LOWER(
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END
+ )`,
+ },
+ { field: "Client", column: `LOWER(a."Client")` },
+ { field: "DeviceName", column: `LOWER(a."DeviceName")` },
+ { field: "ActivityDateInserted", column: "a.ActivityDateInserted", isColumn: true },
+ { field: "PlaybackDuration", column: `a.PlaybackDuration`, isColumn: true, applyToCTE: true },
+ { field: "TotalPlays", column: `COALESCE("TotalPlays",1)` },
+];
+
//Functions
-function groupActivity(rows) {
- const groupedResults = {};
- rows.forEach((row) => {
- const key = row.NowPlayingItemId + row.EpisodeId + row.UserId;
- if (groupedResults[key]) {
- if (row.ActivityDateInserted > groupedResults[key].ActivityDateInserted) {
- groupedResults[key] = {
- ...row,
- results: groupedResults[key].results,
- };
- }
- groupedResults[key].results.push(row);
- } else {
- groupedResults[key] = {
- ...row,
- results: [],
- };
- groupedResults[key].results.push(row);
- }
- });
-
- // Update GroupedResults with playbackDurationSum
- Object.values(groupedResults).forEach((row) => {
- if (row.results && row.results.length > 0) {
- row.PlaybackDuration = row.results.reduce((acc, item) => acc + parseInt(item.PlaybackDuration), 0);
- row.TotalPlays = row.results.length;
- }
- });
- return groupedResults;
-}
-
function groupRecentlyAdded(rows) {
const groupedResults = {};
rows.forEach((row) => {
@@ -122,11 +134,102 @@ async function purgeLibraryItems(id, withActivity, purgeAll = false) {
text: `DELETE FROM jf_playback_activity WHERE${
episodeIds.length > 0 ? ` "EpisodeId" IN (${pgp.as.csv(episodeIds)}) OR` : ""
}${seasonIds.length > 0 ? ` "SeasonId" IN (${pgp.as.csv(seasonIds)}) OR` : ""} "NowPlayingItemId"='${id}'`,
+ refreshViews: true,
};
await db.query(deleteQuery);
}
}
+function buildFilterList(query, filtersArray) {
+ if (filtersArray.length > 0) {
+ query.where = query.where || [];
+ filtersArray.forEach((filter) => {
+ const findField = filterFields.find((item) => item.field === filter.field);
+ const column = findField?.column || "a.ActivityDateInserted";
+ const isColumn = findField?.isColumn || false;
+ const applyToCTE = findField?.applyToCTE || false;
+ if (filter.min) {
+ query.where.push({
+ column: column,
+ operator: ">=",
+ value: `$${query.values.length + 1}`,
+ });
+
+ query.values.push(filter.min);
+
+ if (applyToCTE) {
+ if (query.cte) {
+ if (!query.cte.where) {
+ query.cte.where = [];
+ }
+ query.cte.where.push({
+ column: column,
+ operator: ">=",
+ value: `$${query.values.length + 1}`,
+ });
+
+ query.values.push(filter.min);
+ }
+ }
+ }
+
+ if (filter.max) {
+ query.where.push({
+ column: column,
+ operator: "<=",
+ value: `$${query.values.length + 1}`,
+ });
+
+ query.values.push(filter.max);
+
+ if (applyToCTE) {
+ if (query.cte) {
+ if (!query.cte.where) {
+ query.cte.where = [];
+ }
+ query.cte.where.push({
+ column: column,
+ operator: "<=",
+ value: `$${query.values.length + 1}`,
+ });
+
+ query.values.push(filter.max);
+ }
+ }
+ }
+
+ if (filter.value) {
+ const whereClause = {
+ operator: "LIKE",
+ value: `$${query.values.length + 1}`,
+ };
+
+ query.values.push(`%${filter.value.toLowerCase()}%`);
+
+ if (isColumn) {
+ whereClause.column = column;
+ } else {
+ whereClause.field = column;
+ }
+ query.where.push(whereClause);
+
+ if (applyToCTE) {
+ if (query.cte) {
+ if (!query.cte.where) {
+ query.cte.where = [];
+ }
+ whereClause.value = `$${query.values.length + 1}`;
+ query.cte.where.push(whereClause);
+
+ query.values.push(`%${filter.value.toLowerCase()}%`);
+ }
+ }
+ }
+ });
+ }
+}
+
+//////////////////////////////
router.get("/getconfig", async (req, res) => {
try {
const config = await new configClass().getConfig();
@@ -952,6 +1055,7 @@ router.delete("/item/purge", async (req, res) => {
}${
seasons.length > 0 ? ` "SeasonId" IN (${pgp.as.csv(seasons.map((item) => item.SeasonId))}) OR` : ""
} "NowPlayingItemId"='${id}'`,
+ refreshViews: true,
};
await db.query(deleteQuery);
}
@@ -1085,20 +1189,150 @@ router.post("/setExcludedBackupTable", async (req, res) => {
//DB Queries - History
router.get("/getHistory", async (req, res) => {
+ const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
+
+ let filtersArray = [];
+ if (filters) {
+ try {
+ filtersArray = JSON.parse(filters);
+ } catch (error) {
+ return res.status(400).json({
+ error: "Invalid filters parameter",
+ example: [
+ {
+ field: "ActivityDateInserted",
+ min: "2024-12-31T22:00:00.000Z",
+ max: "2024-12-31T22:00:00.000Z",
+ },
+ {
+ field: "PlaybackDuration",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "TotalPlays",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "DeviceName",
+ value: "test",
+ },
+ {
+ field: "Client",
+ value: "test",
+ },
+ {
+ field: "NowPlayingItemName",
+ value: "test",
+ },
+ {
+ field: "RemoteEndPoint",
+ value: "127.0.0.1",
+ },
+ {
+ field: "UserName",
+ value: "test",
+ },
+ ],
+ });
+ }
+ }
+
+ const sortField = groupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
+
+ const values = [];
+
try {
- const { rows } = await db.query(`
- SELECT a.*, e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber" , i."ParentId"
- FROM jf_playback_activity a
- left join jf_library_episodes e
- on a."EpisodeId"=e."EpisodeId"
- and a."SeasonId"=e."SeasonId"
- left join jf_library_items i
- on i."Id"=a."NowPlayingItemId" or e."SeriesId"=i."Id"
- order by a."ActivityDateInserted" desc`);
+ const cte = {
+ cteAlias: "activity_results",
+ select: [
+ "a.NowPlayingItemId",
+ `COALESCE(a."EpisodeId", '1') as "EpisodeId"`,
+ "a.UserId",
+ `json_agg(row_to_json(a) ORDER BY "ActivityDateInserted" DESC) as results`,
+ `COUNT(a.*) as "TotalPlays"`,
+ `SUM(a."PlaybackDuration") as "TotalDuration"`,
+ ],
+ table: "jf_playback_activity_with_metadata",
+ alias: "a",
+ group_by: ["a.NowPlayingItemId", `COALESCE(a."EpisodeId", '1')`, "a.UserId"],
+ };
- const groupedResults = groupActivity(rows);
+ const query = {
+ cte: cte,
+ select: [
+ "a.*",
+ "a.EpisodeNumber",
+ "a.SeasonNumber",
+ "a.ParentId",
+ "ar.results",
+ "ar.TotalPlays",
+ "ar.TotalDuration",
+ `
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END AS "FullName"
+ `,
+ ],
+ table: "js_latest_playback_activity",
+ alias: "a",
+ joins: [
+ {
+ type: "left",
+ table: "activity_results",
+ alias: "ar",
+ conditions: [
+ { first: "a.NowPlayingItemId", operator: "=", second: "ar.NowPlayingItemId" },
+ { first: "a.EpisodeId", operator: "=", second: "ar.EpisodeId", type: "and" },
+ { first: "a.UserId", operator: "=", second: "ar.UserId", type: "and" },
+ ],
+ },
+ ],
- res.send(Object.values(groupedResults));
+ order_by: sortField,
+ sort_order: desc ? "desc" : "asc",
+ pageNumber: page,
+ pageSize: size,
+ };
+
+ if (search && search.length > 0) {
+ query.where = [
+ {
+ field: `LOWER(
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END
+ )`,
+ operator: "LIKE",
+ value: `$${values.length + 1}`,
+ },
+ ];
+
+ values.push(`%${search.toLowerCase()}%`);
+ }
+
+ query.values = values;
+
+ buildFilterList(query, filtersArray);
+ const result = await dbHelper.query(query);
+
+ result.results = result.results.map((item) => ({
+ ...item,
+ PlaybackDuration: item.TotalDuration ? item.TotalDuration : item.PlaybackDuration,
+ }));
+ const response = { current_page: page, pages: result.pages, size: size, sort: sort, desc: desc, results: result.results };
+ if (search && search.length > 0) {
+ response.search = search;
+ }
+
+ if (filtersArray.length > 0) {
+ response.filters = filtersArray;
+ }
+
+ res.send(response);
} catch (error) {
console.log(error);
}
@@ -1106,6 +1340,55 @@ router.get("/getHistory", async (req, res) => {
router.post("/getLibraryHistory", async (req, res) => {
try {
+ const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
+
+ let filtersArray = [];
+ if (filters) {
+ try {
+ filtersArray = JSON.parse(filters);
+ } catch (error) {
+ return res.status(400).json({
+ error: "Invalid filters parameter",
+ example: [
+ {
+ field: "ActivityDateInserted",
+ min: "2024-12-31T22:00:00.000Z",
+ max: "2024-12-31T22:00:00.000Z",
+ },
+ {
+ field: "PlaybackDuration",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "TotalPlays",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "DeviceName",
+ value: "test",
+ },
+ {
+ field: "Client",
+ value: "test",
+ },
+ {
+ field: "NowPlayingItemName",
+ value: "test",
+ },
+ {
+ field: "RemoteEndPoint",
+ value: "127.0.0.1",
+ },
+ {
+ field: "UserName",
+ value: "test",
+ },
+ ],
+ });
+ }
+ }
const { libraryid } = req.body;
if (libraryid === undefined) {
@@ -1114,20 +1397,109 @@ router.post("/getLibraryHistory", async (req, res) => {
return;
}
- const { rows } = await db.query(
- `select a.* , e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber"
- from jf_playback_activity a
- join jf_library_items i
- on i."Id"=a."NowPlayingItemId"
- left join jf_library_episodes e
- on a."EpisodeId"=e."EpisodeId"
- and a."SeasonId"=e."SeasonId"
- where i."ParentId"=$1
- order by a."ActivityDateInserted" desc`,
- [libraryid]
- );
- const groupedResults = groupActivity(rows);
- res.send(Object.values(groupedResults));
+ const sortField = groupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
+ const values = [];
+
+ const cte = {
+ cteAlias: "activity_results",
+ select: [
+ "a.NowPlayingItemId",
+ `COALESCE(a."EpisodeId", '1') as "EpisodeId"`,
+ "a.UserId",
+ `json_agg(row_to_json(a) ORDER BY "ActivityDateInserted" DESC) as results`,
+ `COUNT(a.*) as "TotalPlays"`,
+ `SUM(a."PlaybackDuration") as "TotalDuration"`,
+ ],
+ table: "jf_playback_activity_with_metadata",
+ alias: "a",
+ group_by: ["a.NowPlayingItemId", `COALESCE(a."EpisodeId", '1')`, "a.UserId"],
+ };
+
+ const query = {
+ cte: cte,
+ select: [
+ "a.*",
+ "a.EpisodeNumber",
+ "a.SeasonNumber",
+ "a.ParentId",
+ "ar.results",
+ "ar.TotalPlays",
+ "ar.TotalDuration",
+ `
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END AS "FullName"
+ `,
+ ],
+ table: "js_latest_playback_activity",
+ alias: "a",
+ joins: [
+ {
+ type: "inner",
+ table: "jf_library_items",
+ alias: "i",
+ conditions: [
+ { first: "i.Id", operator: "=", second: "a.NowPlayingItemId" },
+ { first: "i.ParentId", operator: "=", value: `$${values.length + 1}` },
+ ],
+ },
+ {
+ type: "left",
+ table: "activity_results",
+ alias: "ar",
+ conditions: [
+ { first: "a.NowPlayingItemId", operator: "=", second: "ar.NowPlayingItemId" },
+ { first: "a.EpisodeId", operator: "=", second: "ar.EpisodeId", type: "and" },
+ { first: "a.UserId", operator: "=", second: "ar.UserId", type: "and" },
+ ],
+ },
+ ],
+
+ order_by: sortField,
+ sort_order: desc ? "desc" : "asc",
+ pageNumber: page,
+ pageSize: size,
+ };
+
+ values.push(libraryid);
+
+ if (search && search.length > 0) {
+ query.where = [
+ {
+ field: `LOWER(
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END
+ )`,
+ operator: "LIKE",
+ value: `$${values.length + 1}`,
+ },
+ ];
+
+ values.push(`%${search.toLowerCase()}%`);
+ }
+
+ query.values = values;
+
+ buildFilterList(query, filtersArray);
+
+ const result = await dbHelper.query(query);
+
+ result.results = result.results.map((item) => ({
+ ...item,
+ PlaybackDuration: item.TotalDuration ? item.TotalDuration : item.PlaybackDuration,
+ }));
+
+ const response = { current_page: page, pages: result.pages, size: size, sort: sort, desc: desc, results: result.results };
+ if (search && search.length > 0) {
+ response.search = search;
+ }
+ if (filtersArray.length > 0) {
+ response.filters = filtersArray;
+ }
+ res.send(response);
} catch (error) {
console.log(error);
res.status(503);
@@ -1137,6 +1509,7 @@ router.post("/getLibraryHistory", async (req, res) => {
router.post("/getItemHistory", async (req, res) => {
try {
+ const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
const { itemid } = req.body;
if (itemid === undefined) {
@@ -1145,23 +1518,119 @@ router.post("/getItemHistory", async (req, res) => {
return;
}
- const { rows } = await db.query(
- `select a.*, e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber"
- from jf_playback_activity a
- left join jf_library_episodes e
- on a."EpisodeId"=e."EpisodeId"
- and a."SeasonId"=e."SeasonId"
- where
- (a."EpisodeId"=$1 OR a."SeasonId"=$1 OR a."NowPlayingItemId"=$1);`,
- [itemid]
- );
+ let filtersArray = [];
+ if (filters) {
+ try {
+ filtersArray = JSON.parse(filters);
+ filtersArray = filtersArray.filter((filter) => filter.field !== "TotalPlays");
+ } catch (error) {
+ return res.status(400).json({
+ error: "Invalid filters parameter",
+ example: [
+ {
+ field: "ActivityDateInserted",
+ min: "2024-12-31T22:00:00.000Z",
+ max: "2024-12-31T22:00:00.000Z",
+ },
+ {
+ field: "PlaybackDuration",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "TotalPlays",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "DeviceName",
+ value: "test",
+ },
+ {
+ field: "Client",
+ value: "test",
+ },
+ {
+ field: "NowPlayingItemName",
+ value: "test",
+ },
+ {
+ field: "RemoteEndPoint",
+ value: "127.0.0.1",
+ },
+ {
+ field: "UserName",
+ value: "test",
+ },
+ ],
+ });
+ }
+ }
- const groupedResults = rows.map((item) => ({
- ...item,
- results: [],
- }));
+ const sortField = unGroupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
+ const values = [];
+ const query = {
+ select: [
+ "a.*",
+ "a.EpisodeNumber",
+ "a.SeasonNumber",
+ "a.ParentId",
+ `
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END AS "FullName"
+ `,
+ ],
+ table: "jf_playback_activity_with_metadata",
+ alias: "a",
+ where: [
+ [
+ { column: "a.EpisodeId", operator: "=", value: `$${values.length + 1}` },
+ { column: "a.SeasonId", operator: "=", value: `$${values.length + 2}`, type: "or" },
+ { column: "a.NowPlayingItemId", operator: "=", value: `$${values.length + 3}`, type: "or" },
+ ],
+ ],
+ order_by: sortField,
+ sort_order: desc ? "desc" : "asc",
+ pageNumber: page,
+ pageSize: size,
+ };
- res.send(groupedResults);
+ values.push(itemid);
+ values.push(itemid);
+ values.push(itemid);
+
+ if (search && search.length > 0) {
+ query.where = [
+ {
+ field: `LOWER(
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END
+ )`,
+ operator: "LIKE",
+ value: `$${values.length + 1}`,
+ },
+ ];
+ values.push(`%${search.toLowerCase()}%`);
+ }
+
+ query.values = values;
+ buildFilterList(query, filtersArray);
+ const result = await dbHelper.query(query);
+
+ const response = { current_page: page, pages: result.pages, size: size, sort: sort, desc: desc, results: result.results };
+ if (search && search.length > 0) {
+ response.search = search;
+ }
+
+ if (filters) {
+ response.filters = JSON.parse(filters);
+ }
+
+ res.send(response);
} catch (error) {
console.log(error);
res.status(503);
@@ -1171,6 +1640,56 @@ router.post("/getItemHistory", async (req, res) => {
router.post("/getUserHistory", async (req, res) => {
try {
+ const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
+
+ let filtersArray = [];
+ if (filters) {
+ try {
+ filtersArray = JSON.parse(filters);
+ filtersArray = filtersArray.filter((filter) => filter.field !== "TotalPlays");
+ } catch (error) {
+ return res.status(400).json({
+ error: "Invalid filters parameter",
+ example: [
+ {
+ field: "ActivityDateInserted",
+ min: "2024-12-31T22:00:00.000Z",
+ max: "2024-12-31T22:00:00.000Z",
+ },
+ {
+ field: "PlaybackDuration",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "TotalPlays",
+ min: "1",
+ max: "10",
+ },
+ {
+ field: "DeviceName",
+ value: "test",
+ },
+ {
+ field: "Client",
+ value: "test",
+ },
+ {
+ field: "NowPlayingItemName",
+ value: "test",
+ },
+ {
+ field: "RemoteEndPoint",
+ value: "127.0.0.1",
+ },
+ {
+ field: "UserName",
+ value: "test",
+ },
+ ],
+ });
+ }
+ }
const { userid } = req.body;
if (userid === undefined) {
@@ -1179,21 +1698,66 @@ router.post("/getUserHistory", async (req, res) => {
return;
}
- const { rows } = await db.query(
- `select a.*, e."IndexNumber" "EpisodeNumber",e."ParentIndexNumber" "SeasonNumber" , i."ParentId"
- from jf_playback_activity a
- left join jf_library_episodes e
- on a."EpisodeId"=e."EpisodeId"
- and a."SeasonId"=e."SeasonId"
- left join jf_library_items i
- on i."Id"=a."NowPlayingItemId" or e."SeriesId"=i."Id"
- where a."UserId"=$1;`,
- [userid]
- );
+ const sortField = unGroupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
- const groupedResults = groupActivity(rows);
+ const values = [];
+ const query = {
+ select: [
+ "a.*",
+ "a.EpisodeNumber",
+ "a.SeasonNumber",
+ "a.ParentId",
+ `
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END AS "FullName"
+ `,
+ ],
+ table: "jf_playback_activity_with_metadata",
+ alias: "a",
+ where: [[{ column: "a.UserId", operator: "=", value: `$${values.length + 1}` }]],
+ order_by: sortField,
+ sort_order: desc ? "desc" : "asc",
+ pageNumber: page,
+ pageSize: size,
+ };
- res.send(Object.values(groupedResults));
+ values.push(userid);
+
+ if (search && search.length > 0) {
+ query.where = [
+ {
+ field: `LOWER(
+ CASE
+ WHEN a."SeriesName" is null THEN a."NowPlayingItemName"
+ ELSE CONCAT(a."SeriesName" , ' : S' , a."SeasonNumber" , 'E' , a."EpisodeNumber" , ' - ' , a."NowPlayingItemName")
+ END
+ )`,
+ operator: "LIKE",
+ value: `$${values.length + 1}`,
+ },
+ ];
+ values.push(`%${search.toLowerCase()}%`);
+ }
+
+ query.values = values;
+
+ buildFilterList(query, filtersArray);
+
+ const result = await dbHelper.query(query);
+
+ const response = { current_page: page, pages: result.pages, size: size, sort: sort, desc: desc, results: result.results };
+
+ if (search && search.length > 0) {
+ response.search = search;
+ }
+
+ if (filters) {
+ response.filters = JSON.parse(filters);
+ }
+
+ res.send(response);
} catch (error) {
console.log(error);
res.status(503);
@@ -1211,8 +1775,7 @@ router.post("/deletePlaybackActivity", async (req, res) => {
return;
}
- await db.query(`DELETE from jf_playback_activity where "Id" = ANY($1)`, [ids]);
- // const groupedResults = groupActivity(rows);
+ await db.query(`DELETE from jf_playback_activity where "Id" = ANY($1)`, [ids], true);
res.send(`${ids.length} Records Deleted`);
} catch (error) {
console.log(error);
@@ -1221,6 +1784,31 @@ router.post("/deletePlaybackActivity", async (req, res) => {
}
});
+router.post("/getActivityTimeLine", async (req, res) => {
+ try {
+ const { userId, libraries } = req.body;
+
+ if (libraries === undefined || !Array.isArray(libraries)) {
+ res.status(400);
+ res.send("A list of IDs is required. EG: [1,2,3]");
+ return;
+ }
+
+ if (userId === undefined) {
+ res.status(400);
+ res.send("A userId is required.");
+ return;
+ }
+
+ const { rows } = await db.query(`SELECT * FROM fs_get_user_activity($1, $2);`, [userId, libraries]);
+ res.send(rows);
+ } catch (error) {
+ console.log(error);
+ res.status(503);
+ res.send(error);
+ }
+});
+
// Handle other routes
router.use((req, res) => {
res.status(404).send({ error: "Not Found" });
diff --git a/backend/routes/backup.js b/backend/routes/backup.js
index d24ae44..9fb99ea 100644
--- a/backend/routes/backup.js
+++ b/backend/routes/backup.js
@@ -1,18 +1,19 @@
-const express = require("express");
-const { Pool } = require("pg");
-const fs = require("fs");
-const path = require("path");
-const { randomUUID } = require("crypto");
-const multer = require("multer");
+const express = require('express');
+const { Pool } = require('pg');
+const fs = require('fs');
+const path = require('path');
+const { randomUUID } = require('crypto');
+const multer = require('multer');
-const Logging = require("../classes/logging");
-const backup = require("../classes/backup");
-const triggertype = require("../logging/triggertype");
-const taskstate = require("../logging/taskstate");
-const taskName = require("../logging/taskName");
+const Logging = require('../classes/logging');
+const backup = require('../classes/backup');
+const triggertype = require('../logging/triggertype');
+const taskstate = require('../logging/taskstate');
+const taskName = require('../logging/taskName');
+const sanitizeFilename = require('../utils/sanitizer');
-const { sendUpdate } = require("../ws");
-const db = require("../db");
+const { sendUpdate } = require('../ws');
+const db = require('../db');
const router = express.Router();
@@ -21,14 +22,14 @@ const postgresUser = process.env.POSTGRES_USER;
const postgresPassword = process.env.POSTGRES_PASSWORD;
const postgresIp = process.env.POSTGRES_IP;
const postgresPort = process.env.POSTGRES_PORT;
-const postgresDatabase = process.env.POSTGRES_DB || "jfstat";
-const backupfolder = "backup-data";
+const postgresDatabase = process.env.POSTGRES_DB || 'jfstat';
+const backupfolder = 'backup-data';
// Restore function
function readFile(path) {
return new Promise((resolve, reject) => {
- fs.readFile(path, "utf8", (err, data) => {
+ fs.readFile(path, 'utf8', (err, data) => {
if (err) {
reject(err);
return;
@@ -40,8 +41,11 @@ function readFile(path) {
}
async function restore(file, refLog) {
- refLog.logData.push({ color: "lawngreen", Message: "Starting Restore" });
- refLog.logData.push({ color: "yellow", Message: "Restoring from Backup: " + file });
+ refLog.logData.push({ color: 'lawngreen', Message: 'Starting Restore' });
+ refLog.logData.push({
+ color: 'yellow',
+ Message: 'Restoring from Backup: ' + file,
+ });
const pool = new Pool({
user: postgresUser,
password: postgresPassword,
@@ -58,49 +62,57 @@ async function restore(file, refLog) {
// Use await to wait for the Promise to resolve
jsonData = await readFile(backupPath);
} catch (err) {
- refLog.logData.push({ color: "red", key: tableName, Message: `Failed to read backup file` });
+ refLog.logData.push({
+ color: 'red',
+ key: tableName,
+ Message: `Failed to read backup file`,
+ });
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
console.error(err);
}
// console.log(jsonData);
if (!jsonData) {
- console.log("No Data");
+ console.log('No Data');
return;
}
for (let table of jsonData) {
const data = Object.values(table)[0];
const tableName = Object.keys(table)[0];
- refLog.logData.push({ color: "dodgerblue", key: tableName, Message: `Restoring ${tableName}` });
+ refLog.logData.push({
+ color: 'dodgerblue',
+ key: tableName,
+ Message: `Restoring ${tableName}`,
+ });
for (let index in data) {
- const keysWithQuotes = Object.keys(data[index]).map((key) => `"${key}"`);
- const keyString = keysWithQuotes.join(", ");
+ const keysWithQuotes = Object.keys(data[index]).map(key => `"${key}"`);
+ const keyString = keysWithQuotes.join(', ');
- const valuesWithQuotes = Object.values(data[index]).map((col) => {
+ const valuesWithQuotes = Object.values(data[index]).map(col => {
if (col === null) {
- return "NULL";
- } else if (typeof col === "string") {
+ return 'NULL';
+ } else if (typeof col === 'string') {
return `'${col.replace(/'/g, "''")}'`;
- } else if (typeof col === "object") {
+ } else if (typeof col === 'object') {
return `'${JSON.stringify(col).replace(/'/g, "''")}'`;
} else {
return `'${col}'`;
}
});
- const valueString = valuesWithQuotes.join(", ");
+ const valueString = valuesWithQuotes.join(', ');
const query = `INSERT INTO ${tableName} (${keyString}) VALUES(${valueString}) ON CONFLICT DO NOTHING`;
const { rows } = await pool.query(query);
}
}
await pool.end();
- refLog.logData.push({ color: "lawngreen", Message: "Restore Complete" });
+ refLog.logData.push({ color: 'lawngreen', Message: 'Restore Complete' });
}
// Route handler for backup endpoint
-router.get("/beginBackup", async (req, res) => {
+router.get('/beginBackup', async (req, res) => {
try {
const last_execution = await db
.query(
@@ -110,11 +122,11 @@ router.get("/beginBackup", async (req, res) => {
ORDER BY "TimeRun" DESC
LIMIT 1`
)
- .then((res) => res.rows);
+ .then(res => res.rows);
if (last_execution.length !== 0) {
if (last_execution[0].Result === taskstate.RUNNING) {
- sendUpdate("TaskError", "Error: Backup is already running");
+ sendUpdate('TaskError', 'Error: Backup is already running');
res.send();
return;
}
@@ -125,43 +137,50 @@ router.get("/beginBackup", async (req, res) => {
await Logging.insertLog(uuid, triggertype.Manual, taskName.backup);
await backup(refLog);
Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
- res.send("Backup completed successfully");
- sendUpdate("TaskComplete", { message: triggertype + " Backup Completed" });
+ res.send('Backup completed successfully');
+ sendUpdate('TaskComplete', { message: triggertype + ' Backup Completed' });
} catch (error) {
console.error(error);
- res.status(500).send("Backup failed");
+ res.status(500).send('Backup failed');
}
});
-router.get("/restore/:filename", async (req, res) => {
+router.get('/restore/:filename', async (req, res) => {
try {
const uuid = randomUUID();
let refLog = { logData: [], uuid: uuid };
Logging.insertLog(uuid, triggertype.Manual, taskName.restore);
- const filePath = path.join(__dirname, "..", backupfolder, req.params.filename);
+ const filename = sanitizeFilename(req.params.filename);
+ const filePath = path.join(
+ process.cwd(),
+ __dirname,
+ '..',
+ backupfolder,
+ filename
+ );
await restore(filePath, refLog);
Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
- res.send("Restore completed successfully");
- sendUpdate("TaskComplete", { message: "Restore completed successfully" });
+ res.send('Restore completed successfully');
+ sendUpdate('TaskComplete', { message: 'Restore completed successfully' });
} catch (error) {
console.error(error);
- res.status(500).send("Restore failed");
+ res.status(500).send('Restore failed');
}
});
-router.get("/files", (req, res) => {
+router.get('/files', (req, res) => {
try {
- const directoryPath = path.join(__dirname, "..", backupfolder);
+ const directoryPath = path.join(__dirname, '..', backupfolder);
fs.readdir(directoryPath, (err, files) => {
if (err) {
- res.status(500).send("Unable to read directory");
+ res.status(500).send('Unable to read directory');
} else {
const fileData = files
- .filter((file) => file.endsWith(".json"))
- .map((file) => {
+ .filter(file => file.endsWith('.json'))
+ .map(file => {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
return {
@@ -179,20 +198,34 @@ router.get("/files", (req, res) => {
});
//download backup file
-router.get("/files/:filename", (req, res) => {
- const filePath = path.join(__dirname, "..", backupfolder, req.params.filename);
+router.get('/files/:filename', (req, res) => {
+ const filename = sanitizeFilename(req.params.filename);
+ const filePath = path.join(
+ process.cwd(),
+ __dirname,
+ '..',
+ backupfolder,
+ filename
+ );
res.download(filePath);
});
//delete backup
-router.delete("/files/:filename", (req, res) => {
+router.delete('/files/:filename', (req, res) => {
try {
- const filePath = path.join(__dirname, "..", backupfolder, req.params.filename);
+ const filename = sanitizeFilename(req.params.filename);
+ const filePath = path.join(
+ process.cwd(),
+ __dirname,
+ '..',
+ backupfolder,
+ filename
+ );
- fs.unlink(filePath, (err) => {
+ fs.unlink(filePath, err => {
if (err) {
console.error(err);
- res.status(500).send("An error occurred while deleting the file.");
+ res.status(500).send('An error occurred while deleting the file.');
return;
}
@@ -200,13 +233,13 @@ router.delete("/files/:filename", (req, res) => {
res.status(200).send(`${filePath} has been deleted.`);
});
} catch (error) {
- res.status(500).send("An error occurred while deleting the file.");
+ res.status(500).send('An error occurred while deleting the file.');
}
});
const storage = multer.diskStorage({
destination: function (req, file, cb) {
- cb(null, path.join(__dirname, "..", backupfolder)); // Set the destination folder for uploaded files
+ cb(null, path.join(__dirname, '..', backupfolder)); // Set the destination folder for uploaded files
},
filename: function (req, file, cb) {
cb(null, file.originalname); // Set the file name
@@ -215,7 +248,7 @@ const storage = multer.diskStorage({
const upload = multer({ storage: storage });
-router.post("/upload", upload.single("file"), (req, res) => {
+router.post('/upload', upload.single('file'), (req, res) => {
// Handle the uploaded file here
res.json({
fileName: req.file.originalname,
@@ -225,7 +258,7 @@ router.post("/upload", upload.single("file"), (req, res) => {
// Handle other routes
router.use((req, res) => {
- res.status(404).send({ error: "Not Found" });
+ res.status(404).send({ error: 'Not Found' });
});
module.exports = router;
diff --git a/backend/routes/stats.js b/backend/routes/stats.js
index f213bfc..0f8bd98 100644
--- a/backend/routes/stats.js
+++ b/backend/routes/stats.js
@@ -236,7 +236,9 @@ router.post("/getGlobalLibraryStats", async (req, res) => {
router.get("/getLibraryCardStats", async (req, res) => {
try {
- const { rows } = await db.query("select * from js_library_stats_overview");
+ const { rows } = await db.query(
+ `select *, now() - js_library_stats_overview."ActivityDateInserted" AS "LastActivity" from js_library_stats_overview`
+ );
res.send(rows);
} catch (error) {
res.status(503);
@@ -252,7 +254,10 @@ router.post("/getLibraryCardStats", async (req, res) => {
return res.send("Invalid Library Id");
}
- const { rows } = await db.query(`select * from js_library_stats_overview where "Id"=$1`, [libraryid]);
+ const { rows } = await db.query(
+ `select *, now() - js_library_stats_overview."ActivityDateInserted" AS "LastActivity" from js_library_stats_overview where "Id"=$1`,
+ [libraryid]
+ );
res.send(rows[0]);
} catch (error) {
console.log(error);
diff --git a/backend/swagautogen.js b/backend/swagautogen.js
index 3520a8e..76b6ec4 100644
--- a/backend/swagautogen.js
+++ b/backend/swagautogen.js
@@ -1,4 +1,5 @@
const swaggerAutogen = require("swagger-autogen")();
+const fs = require("fs");
const outputFile = "./swagger.json";
const endpointsFiles = ["./server.js"];
@@ -55,4 +56,42 @@ const config = {
module.exports = config;
-swaggerAutogen(outputFile, endpointsFiles, config);
+const modifySwaggerFile = (filePath) => {
+ const swaggerData = JSON.parse(fs.readFileSync(filePath, "utf8"));
+
+ const endpointsToModify = ["/api/getHistory", "/api/getLibraryHistory", "/api/getUserHistory", "/api/getItemHistory"]; // Add more endpoints as needed
+
+ endpointsToModify.forEach((endpoint) => {
+ if (swaggerData.paths[endpoint]) {
+ const methods = Object.keys(swaggerData.paths[endpoint]);
+ methods.forEach((method) => {
+ const parameters = swaggerData.paths[endpoint][method].parameters;
+ if (parameters) {
+ parameters.forEach((param) => {
+ if (param.name === "sort") {
+ param.enum = [
+ "UserName",
+ "RemoteEndPoint",
+ "NowPlayingItemName",
+ "Client",
+ "DeviceName",
+ "ActivityDateInserted",
+ "PlaybackDuration",
+ ];
+
+ if (endpoint.includes("getHistory") || endpoint.includes("getLibraryHistory")) {
+ param.enum.push("TotalPlays");
+ }
+ }
+ });
+ }
+ });
+ }
+ });
+
+ fs.writeFileSync(filePath, JSON.stringify(swaggerData, null, 2));
+};
+
+swaggerAutogen(outputFile, endpointsFiles, config).then(() => {
+ modifySwaggerFile(outputFile);
+});
diff --git a/backend/swagger.json b/backend/swagger.json
index f8ec8c5..28fda5c 100644
--- a/backend/swagger.json
+++ b/backend/swagger.json
@@ -37,7 +37,10 @@
"description": "Jellystat Log Endpoints"
}
],
- "schemes": ["http", "https"],
+ "schemes": [
+ "http",
+ "https"
+ ],
"securityDefinitions": {
"apiKey": {
"type": "apiKey",
@@ -48,7 +51,9 @@
"paths": {
"/auth/login": {
"post": {
- "tags": ["Auth"],
+ "tags": [
+ "Auth"
+ ],
"description": "",
"parameters": [
{
@@ -74,6 +79,9 @@
"401": {
"description": "Unauthorized"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -82,12 +90,17 @@
},
"/auth/isConfigured": {
"get": {
- "tags": ["Auth"],
+ "tags": [
+ "Auth"
+ ],
"description": "",
"responses": {
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -96,7 +109,9 @@
},
"/auth/createuser": {
"post": {
- "tags": ["Auth"],
+ "tags": [
+ "Auth"
+ ],
"description": "",
"parameters": [
{
@@ -122,6 +137,9 @@
"403": {
"description": "Forbidden"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -130,7 +148,9 @@
},
"/auth/configSetup": {
"post": {
- "tags": ["Auth"],
+ "tags": [
+ "Auth"
+ ],
"description": "",
"parameters": [
{
@@ -156,6 +176,9 @@
"400": {
"description": "Bad Request"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -164,7 +187,9 @@
},
"/proxy/web/assets/img/devices/": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"parameters": [
{
@@ -177,6 +202,9 @@
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -185,7 +213,9 @@
},
"/proxy/Items/Images/Backdrop/": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"parameters": [
{
@@ -213,6 +243,9 @@
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -221,7 +254,9 @@
},
"/proxy/Items/Images/Primary/": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"parameters": [
{
@@ -244,6 +279,9 @@
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -252,7 +290,9 @@
},
"/proxy/Users/Images/Primary/": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"parameters": [
{
@@ -275,6 +315,9 @@
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"500": {
"description": "Internal Server Error"
}
@@ -283,12 +326,17 @@
},
"/proxy/getSessions": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"responses": {
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"503": {
"description": "Service Unavailable"
}
@@ -297,12 +345,17 @@
},
"/proxy/getAdminUsers": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"responses": {
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"503": {
"description": "Service Unavailable"
}
@@ -311,7 +364,9 @@
},
"/proxy/getRecentlyAdded": {
"get": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"parameters": [
{
@@ -324,6 +379,9 @@
"200": {
"description": "OK"
},
+ "404": {
+ "description": "Not Found"
+ },
"503": {
"description": "Service Unavailable"
}
@@ -332,7 +390,9 @@
},
"/proxy/validateSettings": {
"post": {
- "tags": ["Proxy"],
+ "tags": [
+ "Proxy"
+ ],
"description": "",
"parameters": [
{
@@ -357,13 +417,18 @@
},
"400": {
"description": "Bad Request"
+ },
+ "404": {
+ "description": "Not Found"
}
}
}
},
"/api/getconfig": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -403,7 +468,9 @@
},
"/api/getLibraries": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -440,7 +507,9 @@
},
"/api/getRecentlyAdded": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -495,7 +564,9 @@
},
"/api/setconfig": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -548,9 +619,68 @@
}
}
},
+ "/api/setExternalUrl": {
+ "post": {
+ "tags": [
+ "API"
+ ],
+ "description": "",
+ "parameters": [
+ {
+ "name": "authorization",
+ "in": "header",
+ "type": "string"
+ },
+ {
+ "name": "x-api-token",
+ "in": "header",
+ "type": "string"
+ },
+ {
+ "name": "req",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "ExternalUrl": {
+ "example": "any"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "400": {
+ "description": "Bad Request"
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Forbidden"
+ },
+ "404": {
+ "description": "Not Found"
+ },
+ "503": {
+ "description": "Service Unavailable"
+ }
+ }
+ }
+ },
"/api/setPreferredAdmin": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -605,7 +735,9 @@
},
"/api/setRequireLogin": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -657,7 +789,9 @@
},
"/api/updateCredentials": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -718,7 +852,9 @@
},
"/api/updatePassword": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -770,7 +906,9 @@
},
"/api/TrackedLibraries": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -810,7 +948,9 @@
},
"/api/setExcludedLibraries": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -862,7 +1002,9 @@
},
"/api/UntrackedUsers": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -902,7 +1044,9 @@
},
"/api/setUntrackedUsers": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -954,7 +1098,9 @@
},
"/api/keys": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -989,7 +1135,9 @@
}
},
"delete": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1039,7 +1187,9 @@
}
},
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1091,7 +1241,9 @@
},
"/api/getTaskSettings": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1131,7 +1283,9 @@
},
"/api/setTaskSettings": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1189,7 +1343,9 @@
},
"/api/CheckForUpdates": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1226,7 +1382,9 @@
},
"/api/getUserDetails": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1281,7 +1439,9 @@
},
"/api/getLibrary": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1336,7 +1496,9 @@
},
"/api/getLibraryItems": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1388,7 +1550,9 @@
},
"/api/getSeasons": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1440,7 +1604,9 @@
},
"/api/getEpisodes": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1492,7 +1658,9 @@
},
"/api/getItemDetails": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1544,7 +1712,9 @@
},
"/api/item/purge": {
"delete": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1602,7 +1772,9 @@
},
"/api/library/purge": {
"delete": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1660,7 +1832,9 @@
},
"/api/libraryItems/purge": {
"delete": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1716,9 +1890,11 @@
}
}
},
- "/api/getHistory": {
+ "/api/getBackupTables": {
"get": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1749,13 +1925,18 @@
},
"404": {
"description": "Not Found"
+ },
+ "503": {
+ "description": "Service Unavailable"
}
}
}
},
- "/api/getLibraryHistory": {
+ "/api/setExcludedBackupTable": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1773,6 +1954,182 @@
"in": "query",
"type": "string"
},
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "table": {
+ "example": "any"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "400": {
+ "description": "Bad Request"
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Forbidden"
+ },
+ "404": {
+ "description": "Not Found"
+ }
+ }
+ }
+ },
+ "/api/getHistory": {
+ "get": {
+ "tags": [
+ "API"
+ ],
+ "description": "",
+ "parameters": [
+ {
+ "name": "authorization",
+ "in": "header",
+ "type": "string"
+ },
+ {
+ "name": "x-api-token",
+ "in": "header",
+ "type": "string"
+ },
+ {
+ "name": "req",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "size",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "type": "string",
+ "enum": [
+ "UserName",
+ "RemoteEndPoint",
+ "NowPlayingItemName",
+ "Client",
+ "DeviceName",
+ "ActivityDateInserted",
+ "PlaybackDuration",
+ "TotalPlays"
+ ]
+ },
+ {
+ "name": "desc",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "400": {
+ "description": "Bad Request"
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Forbidden"
+ },
+ "404": {
+ "description": "Not Found"
+ }
+ }
+ }
+ },
+ "/api/getLibraryHistory": {
+ "post": {
+ "tags": [
+ "API"
+ ],
+ "description": "",
+ "parameters": [
+ {
+ "name": "authorization",
+ "in": "header",
+ "type": "string"
+ },
+ {
+ "name": "x-api-token",
+ "in": "header",
+ "type": "string"
+ },
+ {
+ "name": "req",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "size",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "type": "string",
+ "enum": [
+ "UserName",
+ "RemoteEndPoint",
+ "NowPlayingItemName",
+ "Client",
+ "DeviceName",
+ "ActivityDateInserted",
+ "PlaybackDuration",
+ "TotalPlays"
+ ]
+ },
+ {
+ "name": "desc",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ },
{
"name": "body",
"in": "body",
@@ -1810,7 +2167,9 @@
},
"/api/getItemHistory": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1828,6 +2187,45 @@
"in": "query",
"type": "string"
},
+ {
+ "name": "size",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "type": "string",
+ "enum": [
+ "UserName",
+ "RemoteEndPoint",
+ "NowPlayingItemName",
+ "Client",
+ "DeviceName",
+ "ActivityDateInserted",
+ "PlaybackDuration"
+ ]
+ },
+ {
+ "name": "desc",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ },
{
"name": "body",
"in": "body",
@@ -1865,7 +2263,9 @@
},
"/api/getUserHistory": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1883,6 +2283,45 @@
"in": "query",
"type": "string"
},
+ {
+ "name": "size",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "page",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "search",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "type": "string",
+ "enum": [
+ "UserName",
+ "RemoteEndPoint",
+ "NowPlayingItemName",
+ "Client",
+ "DeviceName",
+ "ActivityDateInserted",
+ "PlaybackDuration"
+ ]
+ },
+ {
+ "name": "desc",
+ "in": "query",
+ "type": "string"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ },
{
"name": "body",
"in": "body",
@@ -1920,7 +2359,9 @@
},
"/api/deletePlaybackActivity": {
"post": {
- "tags": ["API"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -1973,83 +2414,11 @@
}
}
},
- "/sync/beginSync": {
- "get": {
- "tags": ["Sync"],
- "description": "",
- "parameters": [
- {
- "name": "authorization",
- "in": "header",
- "type": "string"
- },
- {
- "name": "x-api-token",
- "in": "header",
- "type": "string"
- },
- {
- "name": "req",
- "in": "query",
- "type": "string"
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- },
- "401": {
- "description": "Unauthorized"
- },
- "403": {
- "description": "Forbidden"
- },
- "404": {
- "description": "Not Found"
- }
- }
- }
- },
- "/sync/beginPartialSync": {
- "get": {
- "tags": ["Sync"],
- "description": "",
- "parameters": [
- {
- "name": "authorization",
- "in": "header",
- "type": "string"
- },
- {
- "name": "x-api-token",
- "in": "header",
- "type": "string"
- },
- {
- "name": "req",
- "in": "query",
- "type": "string"
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- },
- "401": {
- "description": "Unauthorized"
- },
- "403": {
- "description": "Forbidden"
- },
- "404": {
- "description": "Not Found"
- }
- }
- }
- },
- "/sync/fetchItem": {
+ "/api/getActivityTimeLine": {
"post": {
- "tags": ["Sync"],
+ "tags": [
+ "API"
+ ],
"description": "",
"parameters": [
{
@@ -2073,7 +2442,10 @@
"schema": {
"type": "object",
"properties": {
- "itemId": {
+ "userId": {
+ "example": "any"
+ },
+ "libraries": {
"example": "any"
}
}
@@ -2096,55 +2468,17 @@
"404": {
"description": "Not Found"
},
- "500": {
- "description": "Internal Server Error"
- },
"503": {
"description": "Service Unavailable"
}
}
}
},
- "/sync/syncPlaybackPluginData": {
- "get": {
- "tags": ["Sync"],
- "description": "",
- "parameters": [
- {
- "name": "authorization",
- "in": "header",
- "type": "string"
- },
- {
- "name": "x-api-token",
- "in": "header",
- "type": "string"
- },
- {
- "name": "req",
- "in": "query",
- "type": "string"
- }
- ],
- "responses": {
- "200": {
- "description": "OK"
- },
- "401": {
- "description": "Unauthorized"
- },
- "403": {
- "description": "Forbidden"
- },
- "404": {
- "description": "Not Found"
- }
- }
- }
- },
"/stats/getLibraryOverview": {
"get": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2184,7 +2518,9 @@
},
"/stats/getMostViewedByType": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2239,7 +2575,9 @@
},
"/stats/getMostPopularByType": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2294,7 +2632,9 @@
},
"/stats/getMostViewedLibraries": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2346,7 +2686,9 @@
},
"/stats/getMostUsedClient": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2398,7 +2740,9 @@
},
"/stats/getMostActiveUsers": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2450,7 +2794,9 @@
},
"/stats/getPlaybackActivity": {
"get": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2490,7 +2836,9 @@
},
"/stats/getAllUserActivity": {
"get": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2527,7 +2875,9 @@
},
"/stats/getUserLastPlayed": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2579,7 +2929,9 @@
},
"/stats/getGlobalUserStats": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2634,7 +2986,9 @@
},
"/stats/getGlobalItemStats": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2689,7 +3043,9 @@
},
"/stats/getGlobalLibraryStats": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2744,7 +3100,9 @@
},
"/stats/getLibraryCardStats": {
"get": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2782,7 +3140,9 @@
}
},
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2834,7 +3194,9 @@
},
"/stats/getLibraryMetadata": {
"get": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2874,7 +3236,9 @@
},
"/stats/getLibraryItemsWithStats": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2923,7 +3287,9 @@
},
"/stats/getLibraryItemsPlayMethodStats": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -2984,7 +3350,9 @@
},
"/stats/getPlaybackMethodStats": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -3036,7 +3404,9 @@
},
"/stats/getLibraryLastPlayed": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -3088,7 +3458,9 @@
},
"/stats/getViewsOverTime": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -3140,7 +3512,9 @@
},
"/stats/getViewsByDays": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -3192,7 +3566,9 @@
},
"/stats/getViewsByHour": {
"post": {
- "tags": ["Stats"],
+ "tags": [
+ "Stats"
+ ],
"description": "",
"parameters": [
{
@@ -3244,7 +3620,9 @@
},
"/backup/beginBackup": {
"get": {
- "tags": ["Backup"],
+ "tags": [
+ "Backup"
+ ],
"description": "",
"parameters": [
{
@@ -3284,7 +3662,9 @@
},
"/backup/restore/{filename}": {
"get": {
- "tags": ["Backup"],
+ "tags": [
+ "Backup"
+ ],
"description": "",
"parameters": [
{
@@ -3330,7 +3710,9 @@
},
"/backup/files": {
"get": {
- "tags": ["Backup"],
+ "tags": [
+ "Backup"
+ ],
"description": "",
"parameters": [
{
@@ -3370,7 +3752,9 @@
},
"/backup/files/{filename}": {
"get": {
- "tags": ["Backup"],
+ "tags": [
+ "Backup"
+ ],
"description": "",
"parameters": [
{
@@ -3396,6 +3780,9 @@
}
],
"responses": {
+ "200": {
+ "description": "OK"
+ },
"401": {
"description": "Unauthorized"
},
@@ -3408,7 +3795,9 @@
}
},
"delete": {
- "tags": ["Backup"],
+ "tags": [
+ "Backup"
+ ],
"description": "",
"parameters": [
{
@@ -3454,7 +3843,9 @@
},
"/backup/upload": {
"post": {
- "tags": ["Backup"],
+ "tags": [
+ "Backup"
+ ],
"description": "",
"parameters": [
{
@@ -3491,7 +3882,9 @@
},
"/logs/getLogs": {
"get": {
- "tags": ["Logs"],
+ "tags": [
+ "Logs"
+ ],
"description": "",
"parameters": [
{
@@ -3528,7 +3921,9 @@
},
"/utils/geolocateIp": {
"post": {
- "tags": ["Utils"],
+ "tags": [
+ "Utils"
+ ],
"description": "",
"parameters": [
{
@@ -3590,4 +3985,4 @@
"apiKey": []
}
]
-}
+}
\ No newline at end of file
diff --git a/backend/utils/sanitizer.js b/backend/utils/sanitizer.js
new file mode 100644
index 0000000..169beaf
--- /dev/null
+++ b/backend/utils/sanitizer.js
@@ -0,0 +1,7 @@
+const sanitizer = require("sanitize-filename");
+
+const sanitizeFilename = (filename) => {
+ return sanitizer(filename);
+};
+
+module.export = { sanitizeFilename };
diff --git a/backend/version-control.js b/backend/version-control.js
index 99a774e..1cf964a 100644
--- a/backend/version-control.js
+++ b/backend/version-control.js
@@ -1,43 +1,64 @@
-const GitHub = require('github-api');
-const packageJson = require('../package.json');
-const {compareVersions} =require('compare-versions');
+const GitHub = require("github-api");
+const packageJson = require("../package.json");
+const { compareVersions } = require("compare-versions");
+const memoizee = require("memoizee");
async function checkForUpdates() {
const currentVersion = packageJson.version;
- const repoOwner = 'cyfershepard';
- const repoName = 'Jellystat';
+ const repoOwner = "cyfershepard";
+ const repoName = "Jellystat";
const gh = new GitHub();
- let result={current_version: packageJson.version, latest_version:'', message:'', update_available:false};
+ let result = { current_version: packageJson.version, latest_version: "", message: "", update_available: false };
let latestVersion;
try {
- const path = 'package.json';
+ const path = "package.json";
- const response = await gh.getRepo(repoOwner, repoName).getContents('main', path);
+ const response = await gh.getRepo(repoOwner, repoName).getContents("main", path);
const content = response.data.content;
- const decodedContent = Buffer.from(content, 'base64').toString();
+ const decodedContent = Buffer.from(content, "base64").toString();
latestVersion = JSON.parse(decodedContent).version;
- if (compareVersions(latestVersion,currentVersion) > 0) {
+ if (compareVersions(latestVersion, currentVersion) > 0) {
// console.log(`A new version V.${latestVersion} of ${repoName} is available.`);
- result = { current_version: packageJson.version, latest_version: latestVersion, message: `${repoName} has an update ${latestVersion}`, update_available:true };
- } else if (compareVersions(latestVersion,currentVersion) < 0) {
+ result = {
+ current_version: packageJson.version,
+ latest_version: latestVersion,
+ message: `${repoName} has an update ${latestVersion}`,
+ update_available: true,
+ };
+ } else if (compareVersions(latestVersion, currentVersion) < 0) {
// console.log(`${repoName} is using a beta version.`);
- result = { current_version: packageJson.version, latest_version: latestVersion, message: `${repoName} is using a beta version`, update_available:false };
+ result = {
+ current_version: packageJson.version,
+ latest_version: latestVersion,
+ message: `${repoName} is using a beta version`,
+ update_available: false,
+ };
} else {
// console.log(`${repoName} is up to date.`);
- result = { current_version: packageJson.version, latest_version: latestVersion, message: `${repoName} is up to date`, update_available:false };
+ result = {
+ current_version: packageJson.version,
+ latest_version: latestVersion,
+ message: `${repoName} is up to date`,
+ update_available: false,
+ };
}
} catch (error) {
console.error(`Failed to fetch releases for ${repoName}: ${error.message}`);
- result = { current_version: packageJson.version, latest_version: 'N/A', message: `Failed to fetch releases for ${repoName}: ${error.message}`, update_available:false };
+ result = {
+ current_version: packageJson.version,
+ latest_version: "N/A",
+ message: `Failed to fetch releases for ${repoName}: ${error.message}`,
+ update_available: false,
+ };
}
return result;
}
+const memoizedCheckForUpdates = memoizee(checkForUpdates, { maxAge: 300000, promise: true });
-
-module.exports = { checkForUpdates };
+module.exports = { checkForUpdates: memoizedCheckForUpdates };
diff --git a/package-lock.json b/package-lock.json
index 2d78d6c..cec26f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,19 +1,20 @@
{
"name": "jfstat",
- "version": "1.1.2",
+ "version": "1.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jfstat",
- "version": "1.1.2",
+ "version": "1.1.3",
"dependencies": {
- "@emotion/react": "^11.11.4",
- "@emotion/styled": "^11.11.0",
- "@mui/icons-material": "^5.15.14",
- "@mui/material": "^5.15.14",
- "@mui/x-data-grid": "^6.2.1",
- "@mui/x-date-pickers": "^7.0.0",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/icons-material": "^6.3.0",
+ "@mui/lab": "^6.0.0-beta.22",
+ "@mui/material": "^6.3.0",
+ "@mui/x-data-grid": "^7.23.3",
+ "@mui/x-date-pickers": "^7.23.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -41,7 +42,8 @@
"i18next-fs-backend": "^2.3.1",
"i18next-http-backend": "^2.4.3",
"knex": "^2.4.2",
- "material-react-table": "^2.12.1",
+ "material-react-table": "^3.1.0",
+ "memoizee": "^0.4.17",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"passport": "^0.6.0",
@@ -60,10 +62,11 @@
"react-toastify": "^9.1.3",
"recharts": "^2.5.0",
"remixicon-react": "^1.0.0",
+ "sanitize-filename": "^1.6.3",
"semver": "^7.5.3",
"sequelize": "^6.29.0",
"socket.io": "^4.7.2",
- "socket.io-client": "^4.7.2",
+ "socket.io-client": "^4.8.1",
"swagger-autogen": "^2.23.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
@@ -2330,9 +2333,9 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
},
"node_modules/@babel/runtime": {
- "version": "7.24.1",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
- "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
+ "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -2699,15 +2702,15 @@
}
},
"node_modules/@emotion/babel-plugin": {
- "version": "11.11.0",
- "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
- "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
- "@emotion/hash": "^0.9.1",
- "@emotion/memoize": "^0.8.1",
- "@emotion/serialize": "^1.1.2",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
@@ -2717,47 +2720,47 @@
}
},
"node_modules/@emotion/cache": {
- "version": "11.11.0",
- "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
- "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"dependencies": {
- "@emotion/memoize": "^0.8.1",
- "@emotion/sheet": "^1.2.2",
- "@emotion/utils": "^1.2.1",
- "@emotion/weak-memoize": "^0.3.1",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
- "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
},
"node_modules/@emotion/is-prop-valid": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
- "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
+ "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
"dependencies": {
- "@emotion/memoize": "^0.8.1"
+ "@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
- "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/react": {
- "version": "11.11.4",
- "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz",
- "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==",
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
- "@emotion/babel-plugin": "^11.11.0",
- "@emotion/cache": "^11.11.0",
- "@emotion/serialize": "^1.1.3",
- "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
- "@emotion/utils": "^1.2.1",
- "@emotion/weak-memoize": "^0.3.1",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
@@ -2770,33 +2773,33 @@
}
},
"node_modules/@emotion/serialize": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
- "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"dependencies": {
- "@emotion/hash": "^0.9.1",
- "@emotion/memoize": "^0.8.1",
- "@emotion/unitless": "^0.8.1",
- "@emotion/utils": "^1.2.1",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
- "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/styled": {
- "version": "11.11.0",
- "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
- "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
+ "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
- "@emotion/babel-plugin": "^11.11.0",
- "@emotion/is-prop-valid": "^1.2.1",
- "@emotion/serialize": "^1.1.2",
- "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
- "@emotion/utils": "^1.2.1"
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/is-prop-valid": "^1.3.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
@@ -2809,27 +2812,27 @@
}
},
"node_modules/@emotion/unitless": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
- "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
- "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
- "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
- "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
@@ -3257,28 +3260,31 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
- "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+ "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "license": "MIT",
"dependencies": {
- "@floating-ui/utils": "^0.2.1"
+ "@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
- "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
+ "version": "1.6.13",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+ "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "license": "MIT",
"dependencies": {
- "@floating-ui/core": "^1.0.0",
- "@floating-ui/utils": "^0.2.0"
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/react-dom": {
- "version": "2.0.8",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
- "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "license": "MIT",
"dependencies": {
- "@floating-ui/dom": "^1.6.1"
+ "@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
@@ -3286,9 +3292,10 @@
}
},
"node_modules/@floating-ui/utils": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
- "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "license": "MIT"
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
@@ -4185,29 +4192,30 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
},
"node_modules/@mui/base": {
- "version": "5.0.0-beta.40",
- "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
- "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
+ "version": "5.0.0-beta.68",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.68.tgz",
+ "integrity": "sha512-F1JMNeLS9Qhjj3wN86JUQYBtJoXyQvknxlzwNl6eS0ZABo1MiohMONj3/WQzYPSXIKC2bS/ZbyBzdHhi2GnEpA==",
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "@floating-ui/react-dom": "^2.0.8",
- "@mui/types": "^7.2.14",
- "@mui/utils": "^5.15.14",
+ "@babel/runtime": "^7.26.0",
+ "@floating-ui/react-dom": "^2.1.1",
+ "@mui/types": "^7.2.20",
+ "@mui/utils": "^6.3.0",
"@popperjs/core": "^2.11.8",
- "clsx": "^2.1.0",
+ "clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -4216,32 +4224,33 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz",
- "integrity": "sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.1.tgz",
+ "integrity": "sha512-2OmnEyoHpj5//dJJpMuxOeLItCCHdf99pjMFfUFdBteCunAK9jW+PwEo4mtdGcLs7P+IgZ+85ypd52eY4AigoQ==",
+ "license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.14.tgz",
- "integrity": "sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.3.0.tgz",
+ "integrity": "sha512-3uWws6DveDn5KxCS34p+sUNMxehuclQY6OmoJeJJ+Sfg9L7LGBpksY/nX5ywKAqickTZnn+sQyVcp963ep9jvw==",
"dependencies": {
- "@babel/runtime": "^7.23.9"
+ "@babel/runtime": "^7.26.0"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@mui/material": "^5.0.0",
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0"
+ "@mui/material": "^6.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -4249,26 +4258,22 @@
}
}
},
- "node_modules/@mui/material": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz",
- "integrity": "sha512-kEbRw6fASdQ1SQ7LVdWR5OlWV3y7Y54ZxkLzd6LV5tmz+NpO3MJKZXSfgR0LHMP7meKsPiMm4AuzV0pXDpk/BQ==",
+ "node_modules/@mui/lab": {
+ "version": "6.0.0-beta.22",
+ "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.22.tgz",
+ "integrity": "sha512-9nwUfBj+UzoQJOCbqV+JcCSJ74T+gGWrM1FMlXzkahtYUcMN+5Zmh2ArlttW3zv2dZyCzp7K5askcnKF0WzFQg==",
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "@mui/base": "5.0.0-beta.40",
- "@mui/core-downloads-tracker": "^5.15.14",
- "@mui/system": "^5.15.14",
- "@mui/types": "^7.2.14",
- "@mui/utils": "^5.15.14",
- "@types/react-transition-group": "^4.4.10",
- "clsx": "^2.1.0",
- "csstype": "^3.1.3",
- "prop-types": "^15.8.1",
- "react-is": "^18.2.0",
- "react-transition-group": "^4.4.5"
+ "@babel/runtime": "^7.26.0",
+ "@mui/base": "5.0.0-beta.68",
+ "@mui/system": "^6.3.1",
+ "@mui/types": "^7.2.21",
+ "@mui/utils": "^6.3.1",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
@@ -4277,9 +4282,11 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "@mui/material": "^6.3.1",
+ "@mui/material-pigment-css": "^6.3.1",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
@@ -4288,30 +4295,89 @@
"@emotion/styled": {
"optional": true
},
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
"@types/react": {
"optional": true
}
}
},
- "node_modules/@mui/private-theming": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz",
- "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==",
+ "node_modules/@mui/material": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.1.tgz",
+ "integrity": "sha512-ynG9ayhxgCsHJ/dtDcT1v78/r2GwQyP3E0hPz3GdPRl0uFJz/uUTtI5KFYwadXmbC+Uv3bfB8laZ6+Cpzh03gA==",
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "@mui/utils": "^5.15.14",
- "prop-types": "^15.8.1"
+ "@babel/runtime": "^7.26.0",
+ "@mui/core-downloads-tracker": "^6.3.1",
+ "@mui/system": "^6.3.1",
+ "@mui/types": "^7.2.21",
+ "@mui/utils": "^6.3.1",
+ "@popperjs/core": "^2.11.8",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.0.0",
+ "react-transition-group": "^4.4.5"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0"
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material-pigment-css": "^6.3.1",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material/node_modules/react-is": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
+ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
+ "license": "MIT"
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.1.tgz",
+ "integrity": "sha512-g0u7hIUkmXmmrmmf5gdDYv9zdAig0KoxhIQn1JN8IVqApzf/AyRhH3uDGx5mSvs8+a1zb4+0W6LC260SyTTtdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mui/utils": "^6.3.1",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -4320,17 +4386,20 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
- "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.1.tgz",
+ "integrity": "sha512-/7CC0d2fIeiUxN5kCCwYu4AWUDd9cCTxWCyo0v/Rnv6s8uk6hWgJC3VLZBoDENBHf/KjqDZuYJ2CR+7hD6QYww==",
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "@emotion/cache": "^11.11.0",
+ "@babel/runtime": "^7.26.0",
+ "@emotion/cache": "^11.13.5",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
@@ -4339,7 +4408,7 @@
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
- "react": "^17.0.0 || ^18.0.0"
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
@@ -4351,21 +4420,22 @@
}
},
"node_modules/@mui/system": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.14.tgz",
- "integrity": "sha512-auXLXzUaCSSOLqJXmsAaq7P96VPRXg2Rrz6OHNV7lr+kB8lobUF+/N84Vd9C4G/wvCXYPs5TYuuGBRhcGbiBGg==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.1.tgz",
+ "integrity": "sha512-AwqQ3EAIT2np85ki+N15fF0lFXX1iFPqenCzVOSl3QXKy2eifZeGd9dGtt7pGMoFw5dzW4dRGGzRpLAq9rkl7A==",
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "@mui/private-theming": "^5.15.14",
- "@mui/styled-engine": "^5.15.14",
- "@mui/types": "^7.2.14",
- "@mui/utils": "^5.15.14",
- "clsx": "^2.1.0",
+ "@babel/runtime": "^7.26.0",
+ "@mui/private-theming": "^6.3.1",
+ "@mui/styled-engine": "^6.3.1",
+ "@mui/types": "^7.2.21",
+ "@mui/utils": "^6.3.1",
+ "clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
@@ -4374,8 +4444,8 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0"
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
@@ -4390,11 +4460,12 @@
}
},
"node_modules/@mui/types": {
- "version": "7.2.14",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz",
- "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==",
+ "version": "7.2.21",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
+ "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
+ "license": "MIT",
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0"
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -4403,25 +4474,28 @@
}
},
"node_modules/@mui/utils": {
- "version": "5.15.14",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz",
- "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==",
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.1.tgz",
+ "integrity": "sha512-sjGjXAngoio6lniQZKJ5zGfjm+LD2wvLwco7FbKe1fu8A7VIFmz2SwkLb+MDPLNX1lE7IscvNNyh1pobtZg2tw==",
+ "license": "MIT",
"dependencies": {
- "@babel/runtime": "^7.23.9",
- "@types/prop-types": "^15.7.11",
+ "@babel/runtime": "^7.26.0",
+ "@mui/types": "^7.2.21",
+ "@types/prop-types": "^15.7.14",
+ "clsx": "^2.1.1",
"prop-types": "^15.8.1",
- "react-is": "^18.2.0"
+ "react-is": "^19.0.0"
},
"engines": {
- "node": ">=12.0.0"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@types/react": "^17.0.0 || ^18.0.0",
- "react": "^17.0.0 || ^18.0.0"
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -4429,42 +4503,58 @@
}
}
},
+ "node_modules/@mui/utils/node_modules/react-is": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz",
+ "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==",
+ "license": "MIT"
+ },
"node_modules/@mui/x-data-grid": {
- "version": "6.18.0",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.18.0.tgz",
- "integrity": "sha512-js7Qhv+8XLgXilghKZmfBgUbkP7dYt7V1HLkM4C9285jFRUDFgAM9L6PVlRyUMn+YhVVPbvy+SWT+VWC8rYeQQ==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.3.tgz",
+ "integrity": "sha512-EiM5kut6N/0o0iEYx8A7M3fJqknAa1kcPvGhlX3hH50ERLDeuJaqoKzvRYLBbYKWydHIc+0hHIFcK5oQTXLenw==",
"dependencies": {
- "@babel/runtime": "^7.23.2",
- "@mui/utils": "^5.14.16",
- "clsx": "^2.0.0",
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0",
+ "@mui/x-internals": "7.23.0",
+ "clsx": "^2.1.1",
"prop-types": "^15.8.1",
- "reselect": "^4.1.8"
+ "reselect": "^5.1.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
- "@mui/material": "^5.4.1",
- "@mui/system": "^5.4.1",
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
}
},
"node_modules/@mui/x-date-pickers": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.0.0.tgz",
- "integrity": "sha512-/9mp4O2WMixHOso63DBoZVfJVYGrzOHF5voheV2tYQ4XqDdTKp2AdWS3oh8PGwrsvCzqkvb3quzTqhKoEsJUwA==",
+ "version": "7.23.3",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz",
+ "integrity": "sha512-bjTYX/QzD5ZhVZNNnastMUS3j2Hy4p4IXmJgPJ0vKvQBvUdfEO+ZF42r3PJNNde0FVT1MmTzkmdTlz0JZ6ukdw==",
"dependencies": {
- "@babel/runtime": "^7.24.0",
- "@mui/base": "^5.0.0-beta.40",
- "@mui/system": "^5.15.14",
- "@mui/utils": "^5.15.14",
- "@types/react-transition-group": "^4.4.10",
- "clsx": "^2.1.0",
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0",
+ "@mui/x-internals": "7.23.0",
+ "@types/react-transition-group": "^4.4.11",
+ "clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-transition-group": "^4.4.5"
},
@@ -4478,16 +4568,17 @@
"peerDependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
- "@mui/material": "^5.15.14",
- "date-fns": "^2.25.0 || ^3.2.0",
- "date-fns-jalali": "^2.13.0-0",
+ "@mui/material": "^5.15.14 || ^6.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
"moment": "^2.29.4",
- "moment-hijri": "^2.1.2",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
- "react": "^17.0.0 || ^18.0.0",
- "react-dom": "^17.0.0 || ^18.0.0"
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
@@ -4519,6 +4610,25 @@
}
}
},
+ "node_modules/@mui/x-internals": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.0.tgz",
+ "integrity": "sha512-bPclKpqUiJYIHqmTxSzMVZi6MH51cQsn5U+8jskaTlo3J4QiMeCYJn/gn7YbeR9GOZFp8hetyHjoQoVHKRXCig==",
+ "dependencies": {
+ "@babel/runtime": "^7.25.7",
+ "@mui/utils": "^5.16.6 || ^6.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -5286,11 +5396,11 @@
"dev": true
},
"node_modules/@tanstack/match-sorter-utils": {
- "version": "8.11.8",
- "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz",
- "integrity": "sha512-3VPh0SYMGCa5dWQEqNab87UpCMk+ANWHDP4ALs5PeEW9EpfTAbrezzaOk/OiM52IESViefkoAOYuxdoa04p6aA==",
+ "version": "8.19.4",
+ "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz",
+ "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==",
"dependencies": {
- "remove-accents": "0.4.2"
+ "remove-accents": "0.5.0"
},
"engines": {
"node": ">=12"
@@ -5301,11 +5411,11 @@
}
},
"node_modules/@tanstack/react-table": {
- "version": "8.13.2",
- "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.13.2.tgz",
- "integrity": "sha512-b6mR3mYkjRtJ443QZh9sc7CvGTce81J35F/XMr0OoWbx0KIM7TTTdyNP2XKObvkLpYnLpCrYDwI3CZnLezWvpg==",
+ "version": "8.20.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
+ "integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==",
"dependencies": {
- "@tanstack/table-core": "8.13.2"
+ "@tanstack/table-core": "8.20.5"
},
"engines": {
"node": ">=12"
@@ -5315,30 +5425,30 @@
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
- "react": ">=16",
- "react-dom": ">=16"
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.1.3.tgz",
- "integrity": "sha512-YCzcbF/Ws/uZ0q3Z6fagH+JVhx4JLvbSflgldMgLsuvB8aXjZLLb3HvrEVxY480F9wFlBiXlvQxOyXb5ENPrNA==",
+ "version": "3.11.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
+ "integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
"dependencies": {
- "@tanstack/virtual-core": "3.1.3"
+ "@tanstack/virtual-core": "3.11.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
- "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
- "version": "8.13.2",
- "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.13.2.tgz",
- "integrity": "sha512-/2saD1lWBUV6/uNAwrsg2tw58uvMJ07bO2F1IWMxjFRkJiXKQRuc3Oq2aufeobD3873+4oIM/DRySIw7+QsPPw==",
+ "version": "8.20.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
+ "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
"engines": {
"node": ">=12"
},
@@ -5348,9 +5458,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.1.3.tgz",
- "integrity": "sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==",
+ "version": "3.11.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
+ "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
@@ -5797,9 +5907,9 @@
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
},
"node_modules/@types/prop-types": {
- "version": "15.7.12",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
- "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
},
"node_modules/@types/q": {
"version": "1.5.8",
@@ -5835,10 +5945,10 @@
}
},
"node_modules/@types/react-transition-group": {
- "version": "4.4.10",
- "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
- "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
- "dependencies": {
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+ "peerDependencies": {
"@types/react": "*"
}
},
@@ -7465,6 +7575,20 @@
"node": ">=4"
}
},
+ "node_modules/bufferutil": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz",
+ "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
@@ -7736,9 +7860,9 @@
}
},
"node_modules/clsx": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
- "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
@@ -8585,6 +8709,18 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
+ "node_modules/d": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+ "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
@@ -9158,23 +9294,23 @@
}
},
"node_modules/engine.io-client": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz",
- "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==",
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
- "ws": "~8.11.0",
- "xmlhttprequest-ssl": "~2.0.0"
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -9186,29 +9322,9 @@
}
},
"node_modules/engine.io-client/node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
- },
- "node_modules/engine.io-client/node_modules/ws": {
- "version": "8.11.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
- "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/engine.io-parser": {
"version": "5.2.1",
@@ -9442,6 +9558,54 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es5-ext": {
+ "version": "0.10.64",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+ "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+ "dependencies": {
+ "d": "^1.0.2",
+ "ext": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/es6-weak-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+ "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.46",
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.1"
+ }
+ },
"node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
@@ -10057,6 +10221,20 @@
"node": ">=6"
}
},
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -10136,6 +10314,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -10323,6 +10510,14 @@
}
]
},
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
+ "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -11128,12 +11323,12 @@
}
},
"node_modules/highlight-words": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/highlight-words/-/highlight-words-1.2.2.tgz",
- "integrity": "sha512-Mf4xfPXYm8Ay1wTibCrHpNWeR2nUMynMVFkXCi4mbl+TEgmNOe+I4hV7W3OCZcSvzGL6kupaqpfHOemliMTGxQ==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/highlight-words/-/highlight-words-2.0.0.tgz",
+ "integrity": "sha512-If5n+IhSBRXTScE7wl16VPmd+44Vy7kof24EdqhjsZsDuHikpv1OCagVcJFpB4fS4UPUniedlWqrjIO8vWOsIQ==",
"engines": {
- "node": ">= 16",
- "npm": ">= 8"
+ "node": ">= 20",
+ "npm": ">= 9"
}
},
"node_modules/hoist-non-react-statics": {
@@ -11905,6 +12100,11 @@
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="
},
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
+ },
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -14697,6 +14897,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lru-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
+ "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==",
+ "dependencies": {
+ "es5-ext": "~0.10.2"
+ }
+ },
"node_modules/luxon": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
@@ -14754,14 +14962,14 @@
}
},
"node_modules/material-react-table": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-2.12.1.tgz",
- "integrity": "sha512-nqILE26I/6/UfiILEc7mnVhIoVdG78qvkav85dcbn5BbNXkAJeA57Slut50eYytd7aQHCbaq9MoLvsVeO1yaIw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/material-react-table/-/material-react-table-3.1.0.tgz",
+ "integrity": "sha512-/zPn38QhxQE7mkwLex4CojX3UP2+/+/u7NVq7CHS5d6P8LdTteJPVYXVzym/uhaXzAzFB1ojsbP7zI/y6iUdtQ==",
"dependencies": {
- "@tanstack/match-sorter-utils": "8.11.8",
- "@tanstack/react-table": "8.13.2",
- "@tanstack/react-virtual": "3.1.3",
- "highlight-words": "1.2.2"
+ "@tanstack/match-sorter-utils": "8.19.4",
+ "@tanstack/react-table": "8.20.6",
+ "@tanstack/react-virtual": "3.11.2",
+ "highlight-words": "2.0.0"
},
"engines": {
"node": ">=16"
@@ -14771,13 +14979,13 @@
"url": "https://github.com/sponsors/kevinvandy"
},
"peerDependencies": {
- "@emotion/react": ">=11.11",
- "@emotion/styled": ">=11.11",
- "@mui/icons-material": ">=5.11",
- "@mui/material": ">=5.13",
- "@mui/x-date-pickers": ">=6.15.0",
- "react": ">=17.0",
- "react-dom": ">=17.0"
+ "@emotion/react": ">=11.13",
+ "@emotion/styled": ">=11.13",
+ "@mui/icons-material": ">=6",
+ "@mui/material": ">=6",
+ "@mui/x-date-pickers": ">=7.15",
+ "react": ">=18.0",
+ "react-dom": ">=18.0"
}
},
"node_modules/mdn-data": {
@@ -14804,6 +15012,24 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/memoizee": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
+ "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==",
+ "dependencies": {
+ "d": "^1.0.2",
+ "es5-ext": "^0.10.64",
+ "es6-weak-map": "^2.0.3",
+ "event-emitter": "^0.3.5",
+ "is-promise": "^2.2.2",
+ "lru-queue": "^0.1.0",
+ "next-tick": "^1.1.0",
+ "timers-ext": "^0.1.7"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -15093,6 +15319,11 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+ },
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -15148,6 +15379,18 @@
"node": ">= 6.13.0"
}
},
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -18656,9 +18899,9 @@
}
},
"node_modules/remove-accents": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
- "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
},
"node_modules/renderkid": {
"version": "3.0.0",
@@ -18694,9 +18937,9 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/reselect": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
- "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
@@ -18933,6 +19176,14 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
+ "node_modules/sanitize-filename": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz",
+ "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==",
+ "dependencies": {
+ "truncate-utf8-bytes": "^1.0.0"
+ }
+ },
"node_modules/sanitize.css": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz",
@@ -19424,13 +19675,13 @@
}
},
"node_modules/socket.io-client": {
- "version": "4.7.2",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
- "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
- "engine.io-client": "~6.5.2",
+ "engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
@@ -19438,11 +19689,11 @@
}
},
"node_modules/socket.io-client/node_modules/debug": {
- "version": "4.3.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
- "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -19454,9 +19705,9 @@
}
},
"node_modules/socket.io-client/node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
@@ -20701,6 +20952,18 @@
"node": ">=8"
}
},
+ "node_modules/timers-ext": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz",
+ "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
@@ -20802,6 +21065,14 @@
"tree-kill": "cli.js"
}
},
+ "node_modules/truncate-utf8-bytes": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+ "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==",
+ "dependencies": {
+ "utf8-byte-length": "^1.0.1"
+ }
+ },
"node_modules/tryer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz",
@@ -20866,6 +21137,11 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
+ "node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -21162,11 +21438,30 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/utf-8-validate": {
+ "version": "5.0.10",
+ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
+ "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
+ "hasInstallScript": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
"node_modules/utf8": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
"integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg=="
},
+ "node_modules/utf8-byte-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
+ "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -22352,9 +22647,9 @@
}
},
"node_modules/ws": {
- "version": "8.14.2",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
- "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
@@ -22382,9 +22677,9 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"node_modules/xmlhttprequest-ssl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
- "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
diff --git a/package.json b/package.json
index 068b57a..0d10185 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "jfstat",
- "version": "1.1.2",
+ "version": "1.1.3",
"private": true,
"main": "src/index.jsx",
"scripts": {
@@ -15,12 +15,13 @@
"start": "cd backend && node server.js"
},
"dependencies": {
- "@emotion/react": "^11.11.4",
- "@emotion/styled": "^11.11.0",
- "@mui/icons-material": "^5.15.14",
- "@mui/material": "^5.15.14",
- "@mui/x-data-grid": "^6.2.1",
- "@mui/x-date-pickers": "^7.0.0",
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.0",
+ "@mui/icons-material": "^6.3.0",
+ "@mui/lab": "^6.0.0-beta.22",
+ "@mui/material": "^6.3.0",
+ "@mui/x-data-grid": "^7.23.3",
+ "@mui/x-date-pickers": "^7.23.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -48,7 +49,8 @@
"i18next-fs-backend": "^2.3.1",
"i18next-http-backend": "^2.4.3",
"knex": "^2.4.2",
- "material-react-table": "^2.12.1",
+ "material-react-table": "^3.1.0",
+ "memoizee": "^0.4.17",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"passport": "^0.6.0",
@@ -67,10 +69,11 @@
"react-toastify": "^9.1.3",
"recharts": "^2.5.0",
"remixicon-react": "^1.0.0",
+ "sanitize-filename": "^1.6.3",
"semver": "^7.5.3",
"sequelize": "^6.29.0",
"socket.io": "^4.7.2",
- "socket.io-client": "^4.7.2",
+ "socket.io-client": "^4.8.1",
"swagger-autogen": "^2.23.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
diff --git a/public/locales/en-UK/translation.json b/public/locales/en-UK/translation.json
index e7b5bf6..bd7de5e 100644
--- a/public/locales/en-UK/translation.json
+++ b/public/locales/en-UK/translation.json
@@ -8,7 +8,8 @@
"STATISTICS": "Statistics",
"SETTINGS": "Settings",
"ABOUT": "About",
- "LOGOUT": "Logout"
+ "LOGOUT": "Logout",
+ "TIMELINE": "Timeline"
},
"HOME_PAGE": {
"SESSIONS": "Sessions",
@@ -17,7 +18,9 @@
"LIBRARY_OVERVIEW": "Library Overview"
},
"SESSIONS": {
- "NO_SESSIONS": "No Active Sessions Found"
+ "NO_SESSIONS": "No Active Sessions Found",
+ "DIRECT_PLAY": "Direct Play",
+ "TRANSCODE": "Transcode"
},
"STAT_CARDS": {
"MOST_VIEWED_MOVIES": "MOST VIEWED MOVIES",
@@ -75,7 +78,8 @@
"TAB_CONTROLS": {
"OVERVIEW": "Overview",
"ACTIVITY": "Activity",
- "OPTIONS": "Options"
+ "OPTIONS": "Options",
+ "TIMELINE": "Timeline"
},
"ITEM_ACTIVITY": "Item Activity",
"ACTIVITY_TABLE": {
@@ -228,6 +232,10 @@
"GITHUB": "Github",
"Backup": "Backup Jellystat"
},
+ "TIMELINE_PAGE": {
+ "TIMELINE": "Timeline",
+ "EPISODES":"Episodes"
+ },
"SEARCH": "Search",
"TOTAL": "Total",
"LAST": "Last",
@@ -301,5 +309,6 @@
"LONGITUDE": "Longitude",
"TIMEZONE": "Timezone",
"POSTCODE": "Postcode",
- "X_ROWS_SELECTED": "{ROWS} Rows Selected"
-}
+ "X_ROWS_SELECTED": "{ROWS} Rows Selected",
+ "SUBTITLES": "Subtitles"
+}
\ No newline at end of file
diff --git a/public/locales/fr-FR/translation.json b/public/locales/fr-FR/translation.json
index b93c71d..2e7381b 100644
--- a/public/locales/fr-FR/translation.json
+++ b/public/locales/fr-FR/translation.json
@@ -17,7 +17,9 @@
"LIBRARY_OVERVIEW": "Vue d'ensemble des médiathèques"
},
"SESSIONS": {
- "NO_SESSIONS": "Aucune lecture en cours n'a été trouvée"
+ "NO_SESSIONS": "Aucune lecture en cours n'a été trouvée",
+ "DIRECT_PLAY": "",
+ "TRANSCODE": ""
},
"STAT_CARDS": {
"MOST_VIEWED_MOVIES": "FILMS LES PLUS VUS",
@@ -177,7 +179,7 @@
"SIZE": "Taille",
"JELLYFIN_URL": "URL du serveur Jellyfin",
"EMBY_URL": "URL du serveur Emby",
- "EXTERNAL_URL": "External URL",
+ "EXTERNAL_URL": "URL externe",
"API_KEY": "Clé API",
"API_KEYS": "Clés API",
"KEY_NAME": "Nom de la clé",
@@ -301,5 +303,6 @@
"LONGITUDE": "Longitude",
"TIMEZONE": "Fuseau horaire",
"POSTCODE": "Code postal",
- "X_ROWS_SELECTED": "{ROWS} Ligne(s) sélectionnée(s)"
+ "X_ROWS_SELECTED": "{ROWS} Ligne(s) sélectionnée(s)",
+ "SUBTITLES": ""
}
diff --git a/public/locales/it-IT/translation.json b/public/locales/it-IT/translation.json
new file mode 100644
index 0000000..a283733
--- /dev/null
+++ b/public/locales/it-IT/translation.json
@@ -0,0 +1,305 @@
+{
+ "JELLYSTAT": "Jellystat",
+ "MENU_TABS": {
+ "HOME": "Home",
+ "LIBRARIES": "Librerie",
+ "USERS": "Utenti",
+ "ACTIVITY": "Attività",
+ "STATISTICS": "Statistiche",
+ "SETTINGS": "Impostazioni",
+ "ABOUT": "Informazioni",
+ "LOGOUT": "Disconnetti"
+ },
+ "HOME_PAGE": {
+ "SESSIONS": "Sessioni",
+ "RECENTLY_ADDED": "Aggiunti di recente",
+ "WATCH_STATISTIC": "Statistiche di visualizzazione",
+ "LIBRARY_OVERVIEW": "Panoramica della libreria"
+ },
+ "SESSIONS": {
+ "NO_SESSIONS": "Nessuna sessione attiva trovata"
+ },
+ "STAT_CARDS": {
+ "MOST_VIEWED_MOVIES": "FILM PIÙ VISTI",
+ "MOST_POPULAR_MOVIES": "FILM PIÙ POPOLARI",
+ "MOST_VIEWED_SERIES": "SERIE PIÙ VISTE",
+ "MOST_POPULAR_SERIES": "SERIE PIÙ POPOLARI",
+ "MOST_LISTENED_MUSIC": "MUSICA PIÙ ASCOLTATA",
+ "MOST_POPULAR_MUSIC": "MUSICA PIÙ POPOLARE",
+ "MOST_VIEWED_LIBRARIES": "LIBRERIE PIÙ VISTE",
+ "MOST_USED_CLIENTS": "CLIENTS PIÙ UTILIZZATI",
+ "MOST_ACTIVE_USERS": "UTENTI PIÙ ATTIVI"
+ "CONCURRENT_STREAMS": "FLUSSI SIMULTANEI"
+ },
+ "LIBRARY_OVERVIEW": {
+ "MOVIE_LIBRARIES": "LIBRERIE FILM",
+ "SHOW_LIBRARIES": "LIBRERIE SERIE",
+ "MUSIC_LIBRARIES": "LIBRERIE MUSICALI",
+ "MIXED_LIBRARIES": "LIBRERIE MISTE"
+ },
+ "LIBRARY_CARD": {
+ "LIBRARY": "Libreria",
+ "TOTAL_TIME": "Tempo totale",
+ "TOTAL_FILES": "File totali",
+ "LIBRARY_SIZE": "Dimensione libreria",
+ "TOTAL_PLAYBACK": "Riproduzione totale",
+ "LAST_PLAYED": "Ultima riproduzione",
+ "LAST_ACTIVITY": "Ultima attività",
+ "TRACKED": "Tracciato"
+ },
+ "GLOBAL_STATS": {
+ "LAST_24_HRS": "Ultime 24 ore",
+ "LAST_7_DAYS": "Ultimi 7 giorni",
+ "LAST_30_DAYS": "Ultimi 30 giorni",
+ "LAST_180_DAYS": "Ultimi 180 giorni",
+ "LAST_365_DAYS": "Ultimi 365 giorni",
+ "ALL_TIME": "Totale",
+ "ITEM_STATS": "Statistiche elemento"
+ },
+ "ITEM_INFO": {
+ "FILE_PATH": "Percorso file",
+ "FILE_SIZE": "Dimensione file",
+ "RUNTIME": "Durata",
+ "AVERAGE_RUNTIME": "Durata media",
+ "OPEN_IN_JELLYFIN": "Apri in Jellyfin",
+ "ARCHIVED_DATA_OPTIONS": "Opzioni dati archiviati",
+ "PURGE": "Elimina",
+ "CONFIRM_ACTION": "Conferma azione",
+ "CONFIRM_ACTION_MESSAGE": "Sei sicuro di voler eliminare questo elemento",
+ "CONFIRM_ACTION_MESSAGE_2": "e l'attività di riproduzione associata"
+ },
+ "LIBRARY_INFO": {
+ "LIBRARY_STATS": "Statistiche libreria",
+ "LIBRARY_ACTIVITY": "Attività della libreria"
+ },
+ "TAB_CONTROLS": {
+ "OVERVIEW": "Panoramica",
+ "ACTIVITY": "Attività",
+ "OPTIONS": "Opzioni"
+ },
+ "ITEM_ACTIVITY": "Attività elemento",
+ "ACTIVITY_TABLE": {
+ "MODAL": {
+ "HEADER": "Informazioni flusso"
+ },
+ "IP_ADDRESS": "Indirizzo IP",
+ "CLIENT": "Client",
+ "DEVICE": "Dispositivo",
+ "PLAYBACK_DURATION": "Durata riproduzione",
+ "TOTAL_PLAYBACK": "Riproduzione totale",
+ "EXPAND": "Espandi",
+ "COLLAPSE": "Comprimi",
+ "SORT_BY": "Ordina per",
+ "ASCENDING": "Ascendente",
+ "DESCENDING": "Discendente",
+ "CLEAR_SORT": "Cancella ordinamento",
+ "CLEAR_FILTER": "Cancella filtro",
+ "FILTER_BY": "Filtra per",
+ "COLUMN_ACTIONS": "Azioni colonna",
+ "TOGGLE_SELECT_ROW": "Seleziona/Deseleziona riga",
+ "TOGGLE_SELECT_ALL": "Seleziona/Deseleziona tutto",
+ "MIN": "Min",
+ "MAX": "Max"
+ },
+ "TABLE_NAV_BUTTONS": {
+ "FIRST": "Primo",
+ "LAST": "Ultimo",
+ "NEXT": "Successivo",
+ "PREVIOUS": "Precedente"
+ },
+ "PURGE_OPTIONS": {
+ "PURGE_CACHE": "Elimina elemento nella cache",
+ "PURGE_CACHE_WITH_ACTIVITY": "Elimina elemento nella cache e attività di riproduzione",
+ "PURGE_LIBRARY_CACHE": "Elimina libreria e elementi nella cache",
+ "PURGE_LIBRARY_CACHE_WITH_ACTIVITY": "Elimina libreria, elementi e attività nella cache",
+ "PURGE_LIBRARY_ITEMS_CACHE": "Elimina solo elementi nella libreria",
+ "PURGE_LIBRARY_ITEMS_CACHE_WITH_ACTIVITY": "Elimina elementi nella libreria e attività",
+ "PURGE_ACTIVITY": "Sei sicuro di voler eliminare l'attività di riproduzione selezionata?"
+ },
+ "ERROR_MESSAGES": {
+ "FETCH_THIS_ITEM": "Recupera questo elemento da Jellyfin",
+ "NO_ACTIVITY": "Nessuna attività trovata",
+ "NEVER": "Mai",
+ "N/A": "N/D",
+ "NO_STATS": "Nessuna statistica da visualizzare",
+ "NO_BACKUPS": "Nessun backup trovato",
+ "NO_LOGS": "Nessun log trovato",
+ "NO_API_KEYS": "Nessuna chiave trovata",
+ "NETWORK_ERROR": "Impossibile connettersi al server Jellyfin",
+ "INVALID_LOGIN": "Nome utente o password non validi",
+ "INVALID_URL": "Errore {STATUS}: L'URL richiesto non è stato trovato.",
+ "UNAUTHORIZED": "Errore {STATUS}: Non autorizzato",
+ "PASSWORD_LENGTH": "La password deve essere lunga almeno 6 caratteri",
+ "USERNAME_REQUIRED": "Il nome utente è obbligatorio"
+ },
+ "SHOW_ARCHIVED_LIBRARIES": "Mostra librerie archiviate",
+ "HIDE_ARCHIVED_LIBRARIES": "Nascondi librerie archiviate",
+ "UNITS": {
+ "YEAR": "Anno",
+ "YEARS": "Anni",
+ "MONTH": "Mese",
+ "MONTHS": "Mesi",
+ "DAY": "Giorno",
+ "DAYS": "Giorni",
+ "HOUR": "Ora",
+ "HOURS": "Ore",
+ "MINUTE": "Minuto",
+ "MINUTES": "Minuti",
+ "SECOND": "Secondo",
+ "SECONDS": "Secondi",
+ "PLAYS": "Riproduzioni",
+ "ITEMS": "Elementi"
+ "STREAMS": "Flussi"
+ },
+ "USERS_PAGE": {
+ "ALL_USERS": "Tutti gli utenti",
+ "LAST_CLIENT": "Ultimo client",
+ "LAST_SEEN": "Ultima visualizzazione",
+ "AGO": "Fa",
+ "AGO_ALT": "",
+ "USER_STATS": "Statistiche utente",
+ "USER_ACTIVITY": "Attività dell'utente"
+ },
+ "STAT_PAGE": {
+ "STATISTICS": "Statistiche",
+ "DAILY_PLAY_PER_LIBRARY": "Numero di riproduzioni giornaliere per libreria",
+ "PLAY_COUNT_BY": "Conteggio riproduzioni per"
+ },
+ "SETTINGS_PAGE": {
+ "SETTINGS": "Impostazioni",
+ "LANGUAGE": "Lingua",
+ "SELECT_AN_ADMIN": "Seleziona un amministratore preferito",
+ "LIBRARY_SETTINGS": "Impostazioni libreria",
+ "BACKUP": "Backup",
+ "BACKUPS": "Backup",
+ "CHOOSE_FILE": "Scegli file",
+ "LOGS": "Log",
+ "SIZE": "Dimensione",
+ "JELLYFIN_URL": "URL Jellyfin",
+ "EMBY_URL": "URL Emby",
+ "EXTERNAL_URL": "URL esterno",
+ "API_KEY": "Chiave API",
+ "API_KEYS": "Chiavi API",
+ "KEY_NAME": "Nome chiave",
+ "KEY": "Chiave",
+ "NAME": "Nome",
+ "ADD_KEY": "Aggiungi chiave",
+ "DURATION": "Durata",
+ "EXECUTION_TYPE": "Tipo di esecuzione",
+ "RESULTS": "Risultati",
+ "SELECT_ADMIN": "Seleziona account amministratore preferito",
+ "HOUR_FORMAT": "Formato orario",
+ "HOUR_FORMAT_12": "12 ore",
+ "HOUR_FORMAT_24": "24 ore",
+ "SECURITY": "Sicurezza",
+ "CURRENT_PASSWORD": "Password attuale",
+ "NEW_PASSWORD": "Nuova password",
+ "UPDATE": "Aggiorna",
+ "REQUIRE_LOGIN": "Richiedi accesso",
+ "TASK": "Compito",
+ "TASKS": "Compiti",
+ "INTERVAL": "Intervallo",
+ "INTERVALS": {
+ "15_MIN": "15 minuti",
+ "30_MIN": "30 minuti",
+ "1_HOUR": "1 ora",
+ "12_HOURS": "12 ore",
+ "1_DAY": "1 giorno",
+ "1_WEEK": "1 settimana"
+ },
+ "SELECT_LIBRARIES_TO_IMPORT": "Seleziona librerie da importare",
+ "SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "L'attività per gli elementi in queste librerie viene ancora tracciata, anche se non importata.",
+ "DATE_ADDED": "Data aggiunta"
+ },
+ "TASK_TYPE": {
+ "JOB": "Attività",
+ "IMPORT": "Importa"
+ },
+ "TASK_DESCRIPTION": {
+ "PartialJellyfinSync": "Sincronizzazione elementi aggiunti di recente",
+ "JellyfinSync": "Sincronizzazione completa con Jellyfin",
+ "Jellyfin_Playback_Reporting_Plugin_Sync": "Importa dati plugin di riproduzione",
+ "Backup": "Backup Jellystat"
+ },
+ "ABOUT_PAGE": {
+ "ABOUT_JELLYSTAT": "Informazioni su Jellystat",
+ "VERSION": "Versione",
+ "UPDATE_AVAILABLE": "Aggiornamento disponibile",
+ "GITHUB": "Github",
+ "Backup": "Backup Jellystat"
+ },
+ "SEARCH": "Cerca",
+ "TOTAL": "Totale",
+ "LAST": "Ultimo",
+ "SERIES": "Serie",
+ "SEASON": "Stagione",
+ "SEASONS": "Stagioni",
+ "EPISODE": "Episodio",
+ "EPISODES": "Episodi",
+ "MOVIES": "Film",
+ "MUSIC": "Musica",
+ "SONGS": "Canzoni",
+ "FILES": "File",
+ "LIBRARIES": "Librerie",
+ "USER": "Utente",
+ "USERS": "Utenti",
+ "TYPE": "Tipo",
+ "NEW_VERSION_AVAILABLE": "Nuova versione disponibile",
+ "ARCHIVED": "Archiviato",
+ "NOT_ARCHIVED": "Non archiviato",
+ "ALL": "Tutti",
+ "CLOSE": "Chiudi",
+ "TOTAL_PLAYS": "Riproduzioni totali",
+ "TITLE": "Titolo",
+ "VIEWS": "Visualizzazioni",
+ "WATCH_TIME": "Tempo di visualizzazione",
+ "LAST_WATCHED": "Ultima visualizzazione",
+ "MEDIA": "Media",
+ "SAVE": "Salva",
+ "YES": "Sì",
+ "NO": "No",
+ "FILE_NAME": "Nome file",
+ "DATE": "Data",
+ "START": "Inizia",
+ "DOWNLOAD": "Scarica",
+ "RESTORE": "Ripristina",
+ "ACTIONS": "Azioni",
+ "DELETE": "Elimina",
+ "BITRATE": "Bitrate",
+ "CONTAINER": "Contenitore",
+ "VIDEO": "Video",
+ "CODEC": "Codec",
+ "WIDTH": "Larghezza",
+ "HEIGHT": "Altezza",
+ "FRAMERATE": "Frequenza fotogrammi",
+ "DYNAMIC_RANGE": "Gamma dinamica",
+ "ASPECT_RATIO": "Rapporto d'aspetto",
+ "AUDIO": "Audio",
+ "CHANNELS": "Canali",
+ "LANGUAGE": "Lingua",
+ "STREAM_DETAILS": "Dettagli flusso",
+ "SOURCE_DETAILS": "Dettagli sorgente",
+ "DIRECT": "Diretto",
+ "TRANSCODE": "Transcodifica",
+ "USERNAME": "Nome utente",
+ "PASSWORD": "Password",
+ "LOGIN": "Accedi",
+ "FT_SETUP_PROGRESS": "Impostazione iniziale Passo {STEP} di {TOTAL}",
+ "VALIDATING": "Convalida",
+ "SAVE_JELLYFIN_DETAILS": "Salva dettagli Jellyfin",
+ "SETTINGS_SAVED": "Impostazioni salvate",
+ "SUCCESS": "Successo",
+ "PASSWORD_UPDATE_SUCCESS": "Password aggiornata con successo",
+ "CREATE_USER": "Crea utente",
+ "GEOLOCATION_INFO_FOR": "Informazioni geolocalizzazione per",
+ "CITY": "Città",
+ "REGION": "Regione",
+ "COUNTRY": "Paese",
+ "ORGANIZATION": "Organizzazione",
+ "ISP": "ISP",
+ "LATITUDE": "Latitudine",
+ "LONGITUDE": "Longitudine",
+ "TIMEZONE": "Fuso orario",
+ "POSTCODE": "CAP",
+ "X_ROWS_SELECTED": "{ROWS} righe selezionate"
+}
diff --git a/public/locales/zh-CN/translation.json b/public/locales/zh-CN/translation.json
index cb1f548..a16763b 100644
--- a/public/locales/zh-CN/translation.json
+++ b/public/locales/zh-CN/translation.json
@@ -17,7 +17,9 @@
"LIBRARY_OVERVIEW": "媒体库总览"
},
"SESSIONS": {
- "NO_SESSIONS": "未找到活动会话"
+ "NO_SESSIONS": "未找到活动会话",
+ "DIRECT_PLAY": "",
+ "TRANSCODE": ""
},
"STAT_CARDS": {
"MOST_VIEWED_MOVIES": "最多观看电影",
@@ -301,5 +303,6 @@
"LONGITUDE": "经度",
"TIMEZONE": "时区",
"POSTCODE": "邮编",
- "X_ROWS_SELECTED": "已选中 {ROWS} 行"
+ "X_ROWS_SELECTED": "已选中 {ROWS} 行",
+ "SUBTITLES": ""
}
diff --git a/src/App.css b/src/App.css
index 43e53f5..6b7a3ad 100644
--- a/src/App.css
+++ b/src/App.css
@@ -151,3 +151,7 @@ h2 {
.hide-tab-titles {
display: none !important;
}
+
+::placeholder {
+ color: #a7a7a7 !important; /* Replace with your desired color */
+}
diff --git a/src/lib/languages.jsx b/src/lib/languages.jsx
index 033af18..6e7552b 100644
--- a/src/lib/languages.jsx
+++ b/src/lib/languages.jsx
@@ -11,4 +11,8 @@ export const languages = [
id: "zh-CN",
description: "简体中文",
},
+ {
+ id: "it-IT",
+ description: "Italiano",
+ },
];
diff --git a/src/pages/activity.jsx b/src/pages/activity.jsx
index 12eb53f..59ffa0d 100644
--- a/src/pages/activity.jsx
+++ b/src/pages/activity.jsx
@@ -17,6 +17,7 @@ function Activity() {
const [config, setConfig] = useState(null);
const [streamTypeFilter, setStreamTypeFilter] = useState(localStorage.getItem("PREF_ACTIVITY_StreamTypeFilter") ?? "All");
const [searchQuery, setSearchQuery] = useState("");
+ const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery);
const [itemCount, setItemCount] = useState(parseInt(localStorage.getItem("PREF_ACTIVITY_ItemCount") ?? "10"));
const [libraryFilters, setLibraryFilters] = useState(
localStorage.getItem("PREF_ACTIVITY_libraryFilters") != undefined
@@ -25,9 +26,25 @@ function Activity() {
);
const [libraries, setLibraries] = useState([]);
const [showLibraryFilters, setShowLibraryFilters] = useState(false);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
+ const [filterParams, setFilterParams] = useState([]);
+ const [isBusy, setIsBusy] = useState(false);
+
+ const handlePageChange = (newPage) => {
+ setCurrentPage(newPage);
+ };
+
+ const onSortChange = (sort) => {
+ setSorting({ column: sort.column, desc: sort.desc });
+ };
+
+ const onFilterChange = (filter) => {
+ setFilterParams(filter);
+ };
function setItemLimit(limit) {
- setItemCount(limit);
+ setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
}
@@ -51,6 +68,16 @@ function Activity() {
}
};
+ useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedSearchQuery(searchQuery);
+ }, 300); // Adjust the delay as needed
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [searchQuery]);
+
useEffect(() => {
const fetchConfig = async () => {
try {
@@ -64,9 +91,22 @@ function Activity() {
};
const fetchHistory = () => {
+ setIsBusy(true);
const url = `/api/getHistory`;
+ if (filterParams) {
+ console.log(JSON.stringify(filterParams));
+ }
+
axios
.get(url, {
+ params: {
+ size: itemCount,
+ page: currentPage,
+ search: debouncedSearchQuery,
+ sort: sorting.column,
+ desc: sorting.desc,
+ filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
+ },
headers: {
Authorization: `Bearer ${config.token}`,
"Content-Type": "application/json",
@@ -74,9 +114,11 @@ function Activity() {
})
.then((data) => {
setData(data.data);
+ setIsBusy(false);
})
.catch((error) => {
console.log(error);
+ setIsBusy(false);
});
};
@@ -111,9 +153,19 @@ function Activity() {
});
};
- if (!data && config) {
- fetchHistory();
- fetchLibraries();
+ if (config) {
+ if (
+ !data ||
+ (data.current_page && data.current_page !== currentPage) ||
+ (data.size && data.size !== itemCount) ||
+ (data?.search ?? "") !== debouncedSearchQuery.trim() ||
+ (data?.sort ?? "") !== sorting.column ||
+ (data?.desc ?? true) !== sorting.desc ||
+ JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
+ ) {
+ fetchHistory();
+ fetchLibraries();
+ }
}
if (!config) {
@@ -122,7 +174,7 @@ function Activity() {
const intervalId = setInterval(fetchHistory, 60000 * 60);
return () => clearInterval(intervalId);
- }, [data, config]);
+ }, [data, config, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data) {
return
{showAlert.message} diff --git a/src/pages/components/settings/backupfiles.jsx b/src/pages/components/settings/backupfiles.jsx index a109afe..c109e3b 100644 --- a/src/pages/components/settings/backupfiles.jsx +++ b/src/pages/components/settings/backupfiles.jsx @@ -133,14 +133,14 @@ function Row(file) {
{showAlert.message}