added sorting to history endpoints and changed Activity tables to apply sorting to api calls

This commit is contained in:
CyferShepard
2025-01-04 03:50:39 +02:00
parent cd0f2ae6a6
commit 76363ea0ba
7 changed files with 203 additions and 45 deletions

View File

@@ -12,7 +12,8 @@ function wrapField(field) {
field.includes("MIN") ||
field.includes("AVG") ||
field.includes("DISTINCT") ||
field.includes("json_agg")
field.includes("json_agg") ||
field.includes("CASE")
) {
return field;
}

View File

@@ -17,6 +17,28 @@ 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: "ar.TotalDuration" },
{ field: "TotalPlays", column: "TotalPlays" },
];
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" },
];
//Functions
function groupRecentlyAdded(rows) {
const groupedResults = {};
@@ -1058,7 +1080,9 @@ router.post("/setExcludedBackupTable", async (req, res) => {
//DB Queries - History
router.get("/getHistory", async (req, res) => {
const { size = 50, page = 1, search } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const sortField = groupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
try {
const cte = {
@@ -1078,7 +1102,21 @@ router.get("/getHistory", async (req, res) => {
const query = {
cte: cte,
select: ["a.*", "a.EpisodeNumber", "a.SeasonNumber", "a.ParentId", "ar.results", "ar.TotalPlays", "ar.TotalDuration"],
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: [
@@ -1094,8 +1132,8 @@ router.get("/getHistory", async (req, res) => {
},
],
order_by: "a.ActivityDateInserted",
sort_order: "desc",
order_by: sortField,
sort_order: desc ? "desc" : "asc",
pageNumber: page,
pageSize: size,
};
@@ -1103,7 +1141,12 @@ router.get("/getHistory", async (req, res) => {
if (search && search.length > 0) {
query.where = [
{
field: `LOWER(COALESCE(a."SeriesName" || ' - ' || a."NowPlayingItemName", a."NowPlayingItemName"))`,
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: `${search.toLowerCase()}`,
},
@@ -1115,10 +1158,11 @@ router.get("/getHistory", async (req, res) => {
...item,
PlaybackDuration: item.TotalDuration ? item.TotalDuration : item.PlaybackDuration,
}));
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
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;
}
res.send(response);
} catch (error) {
console.log(error);
@@ -1127,7 +1171,7 @@ router.get("/getHistory", async (req, res) => {
router.post("/getLibraryHistory", async (req, res) => {
try {
const { size = 50, page = 1, search } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { libraryid } = req.body;
if (libraryid === undefined) {
@@ -1136,6 +1180,8 @@ router.post("/getLibraryHistory", async (req, res) => {
return;
}
const sortField = groupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
const cte = {
cteAlias: "activity_results",
select: [
@@ -1153,7 +1199,21 @@ router.post("/getLibraryHistory", async (req, res) => {
const query = {
cte: cte,
select: ["a.*", "a.EpisodeNumber", "a.SeasonNumber", "a.ParentId", "ar.results", "ar.TotalPlays", "ar.TotalDuration"],
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: [
@@ -1178,8 +1238,8 @@ router.post("/getLibraryHistory", async (req, res) => {
},
],
order_by: "a.ActivityDateInserted",
sort_order: "desc",
order_by: sortField,
sort_order: desc ? "desc" : "asc",
pageNumber: page,
pageSize: size,
};
@@ -1187,9 +1247,14 @@ router.post("/getLibraryHistory", async (req, res) => {
if (search && search.length > 0) {
query.where = [
{
field: `LOWER(COALESCE(a."SeriesName" || ' - ' || a."NowPlayingItemName", a."NowPlayingItemName"))`,
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: `%${search.toLowerCase()}%`,
value: `${search.toLowerCase()}`,
},
];
}
@@ -1201,7 +1266,7 @@ router.post("/getLibraryHistory", async (req, res) => {
PlaybackDuration: item.TotalDuration ? item.TotalDuration : item.PlaybackDuration,
}));
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
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;
}
@@ -1215,7 +1280,7 @@ router.post("/getLibraryHistory", async (req, res) => {
router.post("/getItemHistory", async (req, res) => {
try {
const { size = 50, page = 1, search } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { itemid } = req.body;
if (itemid === undefined) {
@@ -1224,8 +1289,21 @@ router.post("/getItemHistory", async (req, res) => {
return;
}
const sortField = unGroupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
const query = {
select: ["a.*", "a.EpisodeNumber", "a.SeasonNumber", "a.ParentId"],
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: [
@@ -1235,25 +1313,30 @@ router.post("/getItemHistory", async (req, res) => {
{ column: "a.NowPlayingItemId", operator: "=", value: itemid, type: "or" },
],
],
order_by: "ActivityDateInserted",
sort_order: "desc",
order_by: sortField,
sort_order: desc ? "desc" : "asc",
pageNumber: page,
pageSize: size,
};
if (search && search.length > 0) {
query.where.push([
query.where = [
{
field: `LOWER(COALESCE(a."SeriesName" || ' - ' || a."NowPlayingItemName", a."NowPlayingItemName"))`,
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: `%${search.toLowerCase()}%`,
value: `${search.toLowerCase()}`,
},
]);
];
}
const result = await dbHelper.query(query);
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
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;
}
@@ -1267,7 +1350,7 @@ router.post("/getItemHistory", async (req, res) => {
router.post("/getUserHistory", async (req, res) => {
try {
const { size = 50, page = 1, search } = req.query;
const { size = 50, page = 1, search, sort = "ActivityDateInserted", desc = true } = req.query;
const { userid } = req.body;
if (userid === undefined) {
@@ -1276,29 +1359,47 @@ router.post("/getUserHistory", async (req, res) => {
return;
}
const sortField = unGroupedSortMap.find((item) => item.field === sort)?.column || "a.ActivityDateInserted";
const query = {
select: ["a.*", "a.EpisodeNumber", "a.SeasonNumber", "a.ParentId"],
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: userid }]],
order_by: "ActivityDateInserted",
sort_order: "desc",
order_by: sortField,
sort_order: desc ? "desc" : "asc",
pageNumber: page,
pageSize: size,
};
if (search && search.length > 0) {
query.where.push([
query.where = [
{
field: `LOWER(COALESCE(a."SeriesName" || ' - ' || a."NowPlayingItemName", a."NowPlayingItemName"))`,
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: `%${search.toLowerCase()}%`,
value: `${search.toLowerCase()}`,
},
]);
];
}
const result = await dbHelper.query(query);
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
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;

View File

@@ -27,12 +27,17 @@ 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 [isBusy, setIsBusy] = useState(false);
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
const onSortChange = (sort) => {
setSorting({ column: sort.column, desc: sort.desc });
};
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
@@ -82,7 +87,7 @@ function Activity() {
const fetchHistory = () => {
setIsBusy(true);
const url = `/api/getHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}`;
const url = `/api/getHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`;
axios
.get(url, {
headers: {
@@ -136,7 +141,9 @@ function Activity() {
!data ||
(data.current_page && data.current_page !== currentPage) ||
(data.size && data.size !== itemCount) ||
(data.search ? data.search : "") !== debouncedSearchQuery.trim()
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
) {
fetchHistory();
fetchLibraries();
@@ -149,7 +156,7 @@ function Activity() {
const intervalId = setInterval(fetchHistory, 60000 * 60);
return () => clearInterval(intervalId);
}, [data, config, itemCount, currentPage, debouncedSearchQuery]);
}, [data, config, itemCount, currentPage, debouncedSearchQuery, sorting]);
if (!data) {
return <Loading />;
@@ -271,6 +278,7 @@ function Activity() {
data={filteredData}
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
pageCount={data.pages}
isBusy={isBusy}
/>

View File

@@ -70,6 +70,7 @@ export default function ActivityTable(props) {
pageSize: 10,
pageIndex: 0,
});
const [sorting, setSorting] = React.useState([{ id: "Date", desc: true }]);
const [modalState, setModalState] = React.useState(false);
const [modalData, setModalData] = React.useState();
@@ -84,6 +85,7 @@ export default function ActivityTable(props) {
return newPaginationState;
});
};
//IP MODAL
const ipv4Regex = new RegExp(
@@ -188,6 +190,7 @@ export default function ActivityTable(props) {
? row.NowPlayingItemName
: row.SeriesName + " : S" + row.SeasonNumber + "E" + row.EpisodeNumber + " - " + row.NowPlayingItemName
}`,
field: "NowPlayingItemName",
header: i18next.t("TITLE"),
minSize: 300,
Cell: ({ row }) => {
@@ -221,6 +224,7 @@ export default function ActivityTable(props) {
},
{
accessorFn: (row) => new Date(row.ActivityDateInserted),
field: "ActivityDateInserted",
header: i18next.t("DATE"),
size: 110,
filterVariant: "date-range",
@@ -248,6 +252,7 @@ export default function ActivityTable(props) {
},
{
accessorFn: (row) => Number(row.TotalPlays ?? 1),
field: "TotalPlays",
header: i18next.t("TOTAL_PLAYS"),
filterFn: "betweenInclusive",
@@ -255,6 +260,22 @@ export default function ActivityTable(props) {
},
];
const fieldMap = columns.map((column) => {
return { accessorKey: column.accessorKey ?? column.field, header: column.header };
});
const handleSortingChange = (updater) => {
setSorting((old) => {
const newSortingState = typeof updater === "function" ? updater(old) : updater;
const column = newSortingState.length > 0 ? newSortingState[0].id : "Date";
const desc = newSortingState.length > 0 ? newSortingState[0].desc : true;
if (props.onSortChange) {
props.onSortChange({ column: fieldMap.find((field) => field.header == column)?.accessorKey ?? column, desc: desc });
}
return newSortingState;
});
};
useEffect(() => {
setData(props.data);
}, [props.data]);
@@ -279,8 +300,10 @@ export default function ActivityTable(props) {
enableExpandAll: false,
enableExpanding: true,
enableDensityToggle: false,
onSortingChange: handleSortingChange,
enableTopToolbar: Object.keys(rowSelection).length > 0,
manualPagination: true,
manualSorting: true,
autoResetPageIndex: false,
initialState: {
expanded: false,
@@ -354,7 +377,7 @@ export default function ActivityTable(props) {
},
},
},
state: { rowSelection, pagination },
state: { rowSelection, pagination, sorting },
filterFromLeafRows: true,
getSubRows: (row) => {
if (Array.isArray(row.results) && row.results.length == 1) {

View File

@@ -15,11 +15,17 @@ function ItemActivity(props) {
const [streamTypeFilter, setStreamTypeFilter] = useState("All");
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [isBusy, setIsBusy] = useState(false);
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
const onSortChange = (sort) => {
setSorting({ column: sort.column, desc: sort.desc });
};
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_ACTIVITY_ItemCount", limit);
@@ -53,7 +59,7 @@ function ItemActivity(props) {
try {
setIsBusy(true);
const itemData = await axios.post(
`/api/getItemHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}`,
`/api/getItemHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
{
itemid: props.itemid,
},
@@ -75,14 +81,16 @@ function ItemActivity(props) {
!data ||
(data.current_page && data.current_page !== currentPage) ||
(data.size && data.size !== itemCount) ||
(data.search ? data.search : "") !== debouncedSearchQuery.trim()
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
) {
fetchData();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
}, [data, props.itemid, token, itemCount, currentPage, debouncedSearchQuery]);
}, [data, props.itemid, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
if (!data || !data.results) {
return <></>;
@@ -168,6 +176,7 @@ function ItemActivity(props) {
data={filteredData}
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
pageCount={data.pages}
isBusy={isBusy}
/>

View File

@@ -18,12 +18,17 @@ function LibraryActivity(props) {
);
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [isBusy, setIsBusy] = useState(false);
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
const onSortChange = (sort) => {
setSorting({ column: sort.column, desc: sort.desc });
};
function setItemLimit(limit) {
setItemCount(parseInt(limit));
localStorage.setItem("PREF_LIBRARY_ACTIVITY_ItemCount", limit);
@@ -61,7 +66,7 @@ function LibraryActivity(props) {
try {
setIsBusy(true);
const libraryData = await axios.post(
`/api/getLibraryHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}`,
`/api/getLibraryHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
{
libraryid: props.LibraryId,
},
@@ -83,14 +88,16 @@ function LibraryActivity(props) {
!data ||
(data.current_page && data.current_page !== currentPage) ||
(data.size && data.size !== itemCount) ||
(data.search ? data.search : "") !== debouncedSearchQuery.trim()
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
) {
fetchData();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
}, [data, props.LibraryId, token, itemCount, currentPage, debouncedSearchQuery]);
}, [data, props.LibraryId, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
if (!data || !data.results) {
return <></>;
@@ -175,6 +182,7 @@ function LibraryActivity(props) {
data={filteredData}
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
pageCount={data.pages}
isBusy={isBusy}
/>

View File

@@ -22,6 +22,7 @@ function UserActivity(props) {
const [showLibraryFilters, setShowLibraryFilters] = useState(false);
const [config, setConfig] = useState();
const [currentPage, setCurrentPage] = useState(1);
const [sorting, setSorting] = useState({ column: "ActivityDateInserted", desc: true });
const [isBusy, setIsBusy] = useState(false);
function setItemLimit(limit) {
@@ -73,12 +74,16 @@ function UserActivity(props) {
setCurrentPage(newPage);
};
const onSortChange = (sort) => {
setSorting({ column: sort.column, desc: sort.desc });
};
useEffect(() => {
const fetchHistory = async () => {
try {
setIsBusy(true);
const itemData = await axios.post(
`/api/getUserHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}`,
`/api/getUserHistory?size=${itemCount}&page=${currentPage}&search=${debouncedSearchQuery}&sort=${sorting.column}&desc=${sorting.desc}`,
{
userid: props.UserId,
},
@@ -125,7 +130,9 @@ function UserActivity(props) {
!data ||
(data.current_page && data.current_page !== currentPage) ||
(data.size && data.size !== itemCount) ||
(data.search ? data.search : "") !== debouncedSearchQuery.trim()
(data?.search ?? "") !== debouncedSearchQuery.trim() ||
(data?.sort ?? "") !== sorting.column ||
(data?.desc ?? true) !== sorting.desc
) {
fetchHistory();
}
@@ -134,7 +141,7 @@ function UserActivity(props) {
const intervalId = setInterval(fetchHistory, 60000 * 5);
return () => clearInterval(intervalId);
}, [props.UserId, token, itemCount, currentPage, debouncedSearchQuery]);
}, [props.UserId, token, itemCount, currentPage, debouncedSearchQuery, sorting]);
if (!data || !data.results) {
return <></>;
@@ -240,6 +247,7 @@ function UserActivity(props) {
data={filteredData}
itemCount={itemCount}
onPageChange={handlePageChange}
onSortChange={onSortChange}
pageCount={data.pages}
isBusy={isBusy}
/>