diff --git a/backend/routes/api.js b/backend/routes/api.js
index eacaf04..a302c12 100644
--- a/backend/routes/api.js
+++ b/backend/routes/api.js
@@ -39,6 +39,25 @@ const unGroupedSortMap = [
{ field: "PlaybackDuration", column: "a.PlaybackDuration" },
];
+const filterFields = [
+ { field: "UserName", column: `LOWER(u."Name")` },
+ { 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 groupRecentlyAdded(rows) {
const groupedResults = {};
@@ -121,6 +140,82 @@ async function purgeLibraryItems(id, withActivity, purgeAll = false) {
}
}
+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: filter.min,
+ });
+
+ if (applyToCTE) {
+ if (query.cte) {
+ if (!query.cte.where) {
+ query.cte.where = [];
+ }
+ query.cte.where.push({
+ column: column,
+ operator: ">=",
+ value: filter.min,
+ });
+ }
+ }
+ }
+
+ if (filter.max) {
+ query.where.push({
+ column: column,
+ operator: "<=",
+ value: filter.max,
+ });
+
+ if (applyToCTE) {
+ if (query.cte) {
+ if (!query.cte.where) {
+ query.cte.where = [];
+ }
+ query.cte.where.push({
+ column: column,
+ operator: "<=",
+ value: filter.max,
+ });
+ }
+ }
+ }
+
+ if (filter.value) {
+ const whereClause = {
+ operator: "LIKE",
+ value: 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 = [];
+ }
+ query.cte.where.push(whereClause);
+ }
+ }
+ }
+ });
+ }
+}
+
+//////////////////////////////
router.get("/getconfig", async (req, res) => {
try {
const config = await new configClass().getConfig();
@@ -1080,7 +1175,55 @@ 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 } = req.query;
+ 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";
@@ -1130,6 +1273,12 @@ router.get("/getHistory", async (req, res) => {
{ first: "a.UserId", operator: "=", second: "ar.UserId", type: "and" },
],
},
+ {
+ type: "left",
+ table: "jf_users",
+ alias: "u",
+ conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
+ },
],
order_by: sortField,
@@ -1152,6 +1301,8 @@ router.get("/getHistory", async (req, res) => {
},
];
}
+
+ buildFilterList(query, filtersArray);
const result = await dbHelper.query(query);
result.results = result.results.map((item) => ({
@@ -1163,6 +1314,10 @@ router.get("/getHistory", async (req, res) => {
response.search = search;
}
+ if (filtersArray.length > 0) {
+ response.filters = filtersArray;
+ }
+
res.send(response);
} catch (error) {
console.log(error);
@@ -1171,7 +1326,55 @@ router.get("/getHistory", async (req, res) => {
router.post("/getLibraryHistory", async (req, res) => {
try {
- const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
+ 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) {
@@ -1236,6 +1439,12 @@ router.post("/getLibraryHistory", async (req, res) => {
{ first: "a.UserId", operator: "=", second: "ar.UserId", type: "and" },
],
},
+ {
+ type: "left",
+ table: "jf_users",
+ alias: "u",
+ conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
+ },
],
order_by: sortField,
@@ -1259,6 +1468,8 @@ router.post("/getLibraryHistory", async (req, res) => {
];
}
+ buildFilterList(query, filtersArray);
+
const result = await dbHelper.query(query);
result.results = result.results.map((item) => ({
@@ -1270,6 +1481,9 @@ router.post("/getLibraryHistory", async (req, res) => {
if (search && search.length > 0) {
response.search = search;
}
+ if (filtersArray.length > 0) {
+ response.filters = filtersArray;
+ }
res.send(response);
} catch (error) {
console.log(error);
@@ -1280,7 +1494,7 @@ router.post("/getLibraryHistory", async (req, res) => {
router.post("/getItemHistory", async (req, res) => {
try {
- const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
+ const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true, filters } = req.query;
const { itemid } = req.body;
if (itemid === undefined) {
@@ -1289,6 +1503,55 @@ router.post("/getItemHistory", async (req, res) => {
return;
}
+ 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 sortField = unGroupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
const query = {
@@ -1306,6 +1569,14 @@ router.post("/getItemHistory", async (req, res) => {
],
table: "jf_playback_activity_with_metadata",
alias: "a",
+ joins: [
+ {
+ type: "left",
+ table: "jf_users",
+ alias: "u",
+ conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
+ },
+ ],
where: [
[
{ column: "a.EpisodeId", operator: "=", value: itemid },
@@ -1334,12 +1605,18 @@ router.post("/getItemHistory", async (req, res) => {
];
}
+ 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);
@@ -1350,7 +1627,56 @@ router.post("/getItemHistory", async (req, res) => {
router.post("/getUserHistory", async (req, res) => {
try {
- const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
+ 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) {
@@ -1376,6 +1702,14 @@ router.post("/getUserHistory", async (req, res) => {
],
table: "jf_playback_activity_with_metadata",
alias: "a",
+ joins: [
+ {
+ type: "left",
+ table: "jf_users",
+ alias: "u",
+ conditions: [{ first: "a.UserId", operator: "=", second: "u.Id" }],
+ },
+ ],
where: [[{ column: "a.UserId", operator: "=", value: userid }]],
order_by: sortField,
sort_order: desc ? "desc" : "asc",
@@ -1397,6 +1731,9 @@ router.post("/getUserHistory", async (req, res) => {
},
];
}
+
+ 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 };
@@ -1405,6 +1742,10 @@ router.post("/getUserHistory", async (req, res) => {
response.search = search;
}
+ if (filters) {
+ response.filters = JSON.parse(filters);
+ }
+
res.send(response);
} catch (error) {
console.log(error);
@@ -1448,7 +1789,7 @@ router.post("/getActivityTimeLine", async (req, res) => {
return;
}
- const {rows} = await db.query(`SELECT * FROM fs_get_user_activity($1, $2);`, [userId, libraries]);
+ const { rows } = await db.query(`SELECT * FROM fs_get_user_activity($1, $2);`, [userId, libraries]);
res.send(rows);
} catch (error) {
console.log(error);
diff --git a/backend/swagger.json b/backend/swagger.json
index d994fa4..28fda5c 100644
--- a/backend/swagger.json
+++ b/backend/swagger.json
@@ -2042,12 +2042,20 @@
"name": "desc",
"in": "query",
"type": "string"
+ },
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
}
],
"responses": {
"200": {
"description": "OK"
},
+ "400": {
+ "description": "Bad Request"
+ },
"401": {
"description": "Unauthorized"
},
@@ -2117,6 +2125,11 @@
"in": "query",
"type": "string"
},
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ },
{
"name": "body",
"in": "body",
@@ -2208,6 +2221,11 @@
"in": "query",
"type": "string"
},
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ },
{
"name": "body",
"in": "body",
@@ -2299,6 +2317,11 @@
"in": "query",
"type": "string"
},
+ {
+ "name": "filters",
+ "in": "query",
+ "type": "string"
+ },
{
"name": "body",
"in": "body",
@@ -2391,6 +2414,66 @@
}
}
},
+ "/api/getActivityTimeLine": {
+ "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": {
+ "userId": {
+ "example": "any"
+ },
+ "libraries": {
+ "example": "any"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ },
+ "400": {
+ "description": "Bad Request"
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "403": {
+ "description": "Forbidden"
+ },
+ "404": {
+ "description": "Not Found"
+ },
+ "503": {
+ "description": "Service Unavailable"
+ }
+ }
+ }
+ },
"/stats/getLibraryOverview": {
"get": {
"tags": [
diff --git a/package-lock.json b/package-lock.json
index 2d341df..a6230ff 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -65,7 +65,7 @@
"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",
@@ -7574,6 +7574,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",
@@ -9279,23 +9293,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"
@@ -9307,29 +9321,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",
@@ -15384,6 +15378,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",
@@ -19660,13 +19666,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": {
@@ -19674,11 +19680,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"
@@ -19690,9 +19696,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",
@@ -21415,6 +21421,20 @@
"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",
@@ -22605,9 +22625,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"
},
@@ -22635,9 +22655,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 3b74515..b279a71 100644
--- a/package.json
+++ b/package.json
@@ -72,7 +72,7 @@
"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/src/pages/activity.jsx b/src/pages/activity.jsx
index 2b40089..59ffa0d 100644
--- a/src/pages/activity.jsx
+++ b/src/pages/activity.jsx
@@ -28,6 +28,7 @@ function Activity() {
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) => {
@@ -38,6 +39,10 @@ function Activity() {
setSorting({ column: sort.column, desc: sort.desc });
};
+ const onFilterChange = (filter) => {
+ setFilterParams(filter);
+ };
+
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
@@ -87,9 +92,21 @@ function Activity() {
const fetchHistory = () => {
setIsBusy(true);
- const url = `/api/getHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`;
+ 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",
@@ -143,7 +160,8 @@ function Activity() {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
- (data?.desc ?? true) !== sorting.desc
+ (data?.desc ?? true) !== sorting.desc ||
+ JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchHistory();
fetchLibraries();
@@ -156,7 +174,7 @@ function Activity() {
const intervalId = setInterval(fetchHistory, 60000 * 60);
return () => clearInterval(intervalId);
- }, [data, config, itemCount, currentPage, debouncedSearchQuery, sorting]);
+ }, [data, config, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data) {
return ;
@@ -279,6 +297,7 @@ function Activity() {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
+ onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>
diff --git a/src/pages/components/activity/activity-table.jsx b/src/pages/components/activity/activity-table.jsx
index 8afbb04..65ed9c8 100644
--- a/src/pages/components/activity/activity-table.jsx
+++ b/src/pages/components/activity/activity-table.jsx
@@ -60,8 +60,6 @@ const token = localStorage.getItem("token");
export default function ActivityTable(props) {
const twelve_hr = JSON.parse(localStorage.getItem("12hr"));
const [data, setData] = React.useState(props.data ?? []);
- const uniqueUserNames = [...new Set(data.map((item) => item.UserName))];
- const uniqueClients = [...new Set(data.map((item) => item.Client))];
const pages = props.pageCount || 1;
const isBusy = props.isBusy;
@@ -72,6 +70,8 @@ export default function ActivityTable(props) {
});
const [sorting, setSorting] = React.useState([{ id: "Date", desc: true }]);
+ const [columnFilters, setColumnFilters] = React.useState([]);
+
const [modalState, setModalState] = React.useState(false);
const [modalData, setModalData] = React.useState();
@@ -152,8 +152,6 @@ export default function ActivityTable(props) {
{
accessorKey: "UserName",
header: i18next.t("USER"),
- filterVariant: "select",
- filterSelectOptions: uniqueUserNames,
Cell: ({ row }) => {
row = row.original;
return (
@@ -207,8 +205,6 @@ export default function ActivityTable(props) {
{
accessorKey: "Client",
header: i18next.t("ACTIVITY_TABLE.CLIENT"),
- filterVariant: "select",
- filterSelectOptions: uniqueClients,
Cell: ({ row }) => {
row = row.original;
return (
@@ -246,8 +242,8 @@ export default function ActivityTable(props) {
accessorKey: "PlaybackDuration",
header: i18next.t("ACTIVITY_TABLE.TOTAL_PLAYBACK"),
minSize: 200,
- filterFn: (row, id, filterValue) => formatTotalWatchTime(row.getValue(id)).startsWith(filterValue),
-
+ // filterFn: (row, id, filterValue) => formatTotalWatchTime(row.getValue(id)).startsWith(filterValue),
+ filterVariant: "range",
Cell: ({ cell }) => {formatTotalWatchTime(cell.getValue())},
},
{
@@ -276,6 +272,35 @@ export default function ActivityTable(props) {
});
};
+ const handleFilteringChange = (updater) => {
+ setColumnFilters((old) => {
+ const newFilterState = typeof updater === "function" ? updater(old) : updater;
+
+ const modifiedFilterState = newFilterState.map((filter) => ({ ...filter }));
+
+ modifiedFilterState.map((filter) => {
+ filter.field = fieldMap.find((field) => field.header == filter.id)?.accessorKey ?? filter.id;
+ delete filter.id;
+ if (Array.isArray(filter.value)) {
+ filter.min = filter.value[0];
+ filter.max = filter.value[1];
+ delete filter.value;
+ } else {
+ const val = filter.value;
+ delete filter.value;
+ filter.value = val;
+ }
+
+ return filter;
+ });
+
+ if (props.onFilterChange) {
+ props.onFilterChange(modifiedFilterState);
+ }
+ return newFilterState;
+ });
+ };
+
useEffect(() => {
setData(props.data);
}, [props.data]);
@@ -300,8 +325,10 @@ export default function ActivityTable(props) {
enableExpandAll: false,
enableExpanding: true,
enableDensityToggle: false,
- enableFilters: false,
+ enableFilters: true,
+ manualFiltering: true,
onSortingChange: handleSortingChange,
+ onColumnFiltersChange: handleFilteringChange,
enableTopToolbar: Object.keys(rowSelection).length > 0,
manualPagination: true,
manualSorting: true,
@@ -378,7 +405,7 @@ export default function ActivityTable(props) {
},
},
},
- state: { rowSelection, pagination, sorting },
+ state: { rowSelection, pagination, sorting, columnFilters },
filterFromLeafRows: true,
getSubRows: (row) => {
if (Array.isArray(row.results) && row.results.length == 1) {
diff --git a/src/pages/components/item-info/item-activity.jsx b/src/pages/components/item-info/item-activity.jsx
index 4cbf38a..ebb52f6 100644
--- a/src/pages/components/item-info/item-activity.jsx
+++ b/src/pages/components/item-info/item-activity.jsx
@@ -16,6 +16,7 @@ function ItemActivity(props) {
const [config, setConfig] = useState();
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) => {
@@ -26,6 +27,10 @@ function ItemActivity(props) {
setSorting({ column: sort.column, desc: sort.desc });
};
+ const onFilterChange = (filter) => {
+ setFilterParams(filter);
+ };
+
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
@@ -59,7 +64,7 @@ function ItemActivity(props) {
try {
setIsBusy(true);
const itemData = await axios.post(
- `/api/getItemHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
+ `/api/getItemHistory`,
{
itemid: props.itemid,
},
@@ -68,6 +73,14 @@ function ItemActivity(props) {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
+ params: {
+ size: itemCount,
+ page: currentPage,
+ search: debouncedSearchQuery,
+ sort: sorting.column,
+ desc: sorting.desc,
+ filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
+ },
}
);
setData(itemData.data);
@@ -83,14 +96,15 @@ function ItemActivity(props) {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
- (data?.desc ?? true) !== sorting.desc
+ (data?.desc ?? true) !== sorting.desc ||
+ JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchData();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
- }, [data, props.itemid, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
+ }, [data, props.itemid, token, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data || !data.results) {
return <>>;
@@ -177,6 +191,7 @@ function ItemActivity(props) {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
+ onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>
diff --git a/src/pages/components/library/library-activity.jsx b/src/pages/components/library/library-activity.jsx
index 3e5b7d0..0139f24 100644
--- a/src/pages/components/library/library-activity.jsx
+++ b/src/pages/components/library/library-activity.jsx
@@ -19,6 +19,7 @@ function LibraryActivity(props) {
const [config, setConfig] = useState();
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) => {
@@ -29,6 +30,10 @@ function LibraryActivity(props) {
setSorting({ column: sort.column, desc: sort.desc });
};
+ const onFilterChange = (filter) => {
+ setFilterParams(filter);
+ };
+
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_LIBRARY_ACTIVITY_ItemCount", limit);
@@ -66,7 +71,7 @@ function LibraryActivity(props) {
try {
setIsBusy(true);
const libraryData = await axios.post(
- `/api/getLibraryHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
+ `/api/getLibraryHistory`,
{
libraryid: props.LibraryId,
},
@@ -75,6 +80,14 @@ function LibraryActivity(props) {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
+ params: {
+ size: itemCount,
+ page: currentPage,
+ search: debouncedSearchQuery,
+ sort: sorting.column,
+ desc: sorting.desc,
+ filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
+ },
}
);
setData(libraryData.data);
@@ -90,14 +103,15 @@ function LibraryActivity(props) {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
- (data?.desc ?? true) !== sorting.desc
+ (data?.desc ?? true) !== sorting.desc ||
+ JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchData();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
- }, [data, props.LibraryId, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
+ }, [data, props.LibraryId, token, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data || !data.results) {
return <>>;
@@ -183,6 +197,7 @@ function LibraryActivity(props) {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
+ onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>
diff --git a/src/pages/components/user-info/user-activity.jsx b/src/pages/components/user-info/user-activity.jsx
index d46dca8..ccdc360 100644
--- a/src/pages/components/user-info/user-activity.jsx
+++ b/src/pages/components/user-info/user-activity.jsx
@@ -23,6 +23,7 @@ function UserActivity(props) {
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
+ const [filterParams, setFilterParams] = useState([]);
const [isBusy, setIsBusy] = useState(false);
function setItemLimit(limit) {
@@ -78,12 +79,16 @@ function UserActivity(props) {
setSorting({ column: sort.column, desc: sort.desc });
};
+ const onFilterChange = (filter) => {
+ setFilterParams(filter);
+ };
+
useEffect(() => {
const fetchHistory = async () => {
try {
setIsBusy(true);
const itemData = await axios.post(
- `/api/getUserHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
+ `/api/getUserHistory`,
{
userid: props.UserId,
},
@@ -92,6 +97,14 @@ function UserActivity(props) {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
+ params: {
+ size: itemCount,
+ page: currentPage,
+ search: debouncedSearchQuery,
+ sort: sorting.column,
+ desc: sorting.desc,
+ filters: filterParams != undefined ? JSON.stringify(filterParams) : null,
+ },
}
);
setData(itemData.data);
@@ -132,7 +145,8 @@ function UserActivity(props) {
(data.size && data.size !== itemCount) ||
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
- (data?.desc ?? true) !== sorting.desc
+ (data?.desc ?? true) !== sorting.desc ||
+ JSON.stringify(data?.filters ?? []) !== JSON.stringify(filterParams ?? [])
) {
fetchHistory();
}
@@ -141,7 +155,7 @@ function UserActivity(props) {
const intervalId = setInterval(fetchHistory, 60000 * 5);
return () => clearInterval(intervalId);
- }, [props.UserId, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
+ }, [props.UserId, token, itemCount, currentPage, debouncedSearchQuery, sorting, filterParams]);
if (!data || !data.results) {
return <>>;
@@ -248,6 +262,7 @@ function UserActivity(props) {
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
+ onFilterChange={onFilterChange}
pageCount={data.pages}
isBusy={isBusy}
/>