mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-19 00:37:22 +01:00
688 lines
18 KiB
JavaScript
688 lines
18 KiB
JavaScript
// api.js
|
|
const express = require("express");
|
|
const db = require("../db");
|
|
const dbHelper = require("../classes/db-helper");
|
|
const moment = require("moment");
|
|
|
|
const router = express.Router();
|
|
|
|
//functions
|
|
function countOverlapsPerHour(records) {
|
|
const hourCounts = {};
|
|
|
|
records.forEach((record) => {
|
|
const start = moment(record.StartTime).subtract(1, "hour");
|
|
const end = moment(record.EndTime).add(1, "hour");
|
|
|
|
// Iterate through each hour from start to end
|
|
for (let hour = start.clone().startOf("hour"); hour.isBefore(end); hour.add(1, "hour")) {
|
|
const hourKey = hour.format("MMM DD, YY HH:00");
|
|
if (!hourCounts[hourKey]) {
|
|
hourCounts[hourKey] = { Transcodes: 0, DirectPlays: 0 };
|
|
}
|
|
if (record.PlayMethod === "Transcode") {
|
|
hourCounts[hourKey].Transcodes++;
|
|
} else {
|
|
hourCounts[hourKey].DirectPlays++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Convert the hourCounts object to an array of key-value pairs, sort it, and convert it back to an object
|
|
const sortedHourCounts = Object.fromEntries(Object.entries(hourCounts).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));
|
|
|
|
return sortedHourCounts;
|
|
}
|
|
|
|
//endpoints
|
|
|
|
router.get("/getLibraryOverview", async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query("SELECT * FROM jf_library_count_view");
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getMostViewedByType", async (req, res) => {
|
|
try {
|
|
const { days, type } = req.body;
|
|
|
|
const valid_types = ["Audio", "Movie", "Series"];
|
|
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
|
|
if (!valid_types.includes(type)) {
|
|
res.status(503);
|
|
return res.send(`Invalid Type Value.\nValid Types: ${JSON.stringify(valid_types)}`);
|
|
}
|
|
if (isNaN(parseFloat(days))) {
|
|
res.status(503);
|
|
return res.send(`Days needs to be a number.`);
|
|
}
|
|
if (Number(days) < 0) {
|
|
res.status(503);
|
|
return res.send(`Days cannot be less than 0`);
|
|
}
|
|
|
|
const { rows } = await db.query(`select * from fs_most_played_items($1,'${type}') limit 5`, [_days - 1]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getMostPopularByType", async (req, res) => {
|
|
try {
|
|
const { days, type } = req.body;
|
|
|
|
const valid_types = ["Audio", "Movie", "Series"];
|
|
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
|
|
if (!valid_types.includes(type)) {
|
|
res.status(503);
|
|
return res.send("Invalid Type Value");
|
|
}
|
|
|
|
const { rows } = await db.query(`select * from fs_most_popular_items($1,$2) limit 5`, [_days - 1, type]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getMostViewedLibraries", async (req, res) => {
|
|
try {
|
|
const { days } = req.body;
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
const { rows } = await db.query(`select * from fs_most_viewed_libraries($1)`, [_days - 1]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getMostUsedClient", async (req, res) => {
|
|
try {
|
|
const { days } = req.body;
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
const { rows } = await db.query(`select * from fs_most_used_clients($1) limit 5`, [_days - 1]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getMostActiveUsers", async (req, res) => {
|
|
try {
|
|
const { days } = req.body;
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
const { rows } = await db.query(`select * from fs_most_active_user($1) limit 5`, [_days - 1]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getPlaybackActivity", async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query("SELECT * FROM jf_playback_activity");
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getAllUserActivity", async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query("SELECT * FROM jf_all_user_activity");
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.send([]);
|
|
}
|
|
});
|
|
|
|
router.post("/getUserLastPlayed", async (req, res) => {
|
|
try {
|
|
const { userid } = req.body;
|
|
const { rows } = await db.query(`select * from fs_last_user_activity($1) limit 15`, [userid]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
//Global Stats
|
|
router.post("/getGlobalUserStats", async (req, res) => {
|
|
try {
|
|
const { hours, userid } = req.body;
|
|
let _hours = hours;
|
|
if (hours === undefined) {
|
|
_hours = 24;
|
|
}
|
|
const { rows } = await db.query(`select * from fs_user_stats($1,$2)`, [_hours, userid]);
|
|
res.send(rows[0]);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getGlobalItemStats", async (req, res) => {
|
|
try {
|
|
const { hours, itemid } = req.body;
|
|
let _hours = hours;
|
|
if (hours === undefined) {
|
|
_hours = 24;
|
|
}
|
|
const { rows } = await db.query(
|
|
`select count(*)"Plays",
|
|
sum("PlaybackDuration") total_playback_duration
|
|
from jf_playback_activity jf_playback_activity
|
|
where
|
|
("EpisodeId"=$1 OR "SeasonId"=$1 OR "NowPlayingItemId"=$1)
|
|
AND jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - INTERVAL '1 hour' * $2 AND NOW();`,
|
|
[itemid, _hours]
|
|
);
|
|
res.send(rows[0]);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getGlobalLibraryStats", async (req, res) => {
|
|
try {
|
|
const { hours, libraryid } = req.body;
|
|
let _hours = hours;
|
|
if (hours === undefined) {
|
|
_hours = 24;
|
|
}
|
|
const { rows } = await db.query(`select * from fs_library_stats($1,$2)`, [_hours, libraryid]);
|
|
res.send(rows[0]);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getLibraryCardStats", async (req, res) => {
|
|
try {
|
|
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);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getLibraryCardStats", async (req, res) => {
|
|
try {
|
|
const { libraryid } = req.body;
|
|
if (libraryid === undefined) {
|
|
res.status(503);
|
|
return res.send("Invalid Library Id");
|
|
}
|
|
|
|
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);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getLibraryMetadata", async (req, res) => {
|
|
try {
|
|
const { rows } = await db.query("select * from js_library_metadata");
|
|
res.send(rows);
|
|
} catch (error) {
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getLibraryItemsWithStats", async (req, res) => {
|
|
try {
|
|
const { libraryid } = req.body;
|
|
const { rows } = await db.query(`SELECT * FROM jf_library_items_with_playcount_playtime where "ParentId"=$1`, [libraryid]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
|
try {
|
|
let { libraryid, startDate, endDate = moment(), hours = 24 } = req.body;
|
|
|
|
// Validate startDate and endDate using moment
|
|
if (
|
|
startDate !== undefined &&
|
|
(!moment(startDate, moment.ISO_8601, true).isValid() || !moment(endDate, moment.ISO_8601, true).isValid())
|
|
) {
|
|
return res.status(400).send({ error: "Invalid date format" });
|
|
}
|
|
|
|
if (hours < 1) {
|
|
return res.status(400).send({ error: "Hours cannot be less than 1" });
|
|
}
|
|
|
|
if (libraryid === undefined) {
|
|
return res.status(400).send({ error: "Invalid Library Id" });
|
|
}
|
|
|
|
if (startDate === undefined) {
|
|
startDate = moment(endDate).subtract(hours, "hour").format("YYYY-MM-DD HH:mm:ss");
|
|
}
|
|
|
|
const { rows } = await db.query(
|
|
`select a.*,i."ParentId"
|
|
from jf_playback_activity a
|
|
left
|
|
join jf_library_episodes e
|
|
on a."EpisodeId"=e."EpisodeId"
|
|
join jf_library_items i
|
|
on i."Id"=a."NowPlayingItemId" or e."SeriesId"=i."Id"
|
|
where i."ParentId"=$1
|
|
and a."ActivityDateInserted" BETWEEN $2 AND $3
|
|
order by a."ActivityDateInserted" desc;
|
|
`,
|
|
[libraryid, startDate, endDate]
|
|
);
|
|
|
|
const stats = rows.map((item) => {
|
|
return {
|
|
Id: item.NowPlayingItemId,
|
|
UserId: item.UserId,
|
|
UserName: item.UserName,
|
|
Client: item.Client,
|
|
DeviceName: item.DeviceName,
|
|
NowPlayingItemName: item.NowPlayingItemName,
|
|
EpisodeId: item.EpisodeId || null,
|
|
SeasonId: item.SeasonId || null,
|
|
StartTime: moment(item.ActivityDateInserted).subtract(item.PlaybackDuration, "seconds").format("YYYY-MM-DD HH:mm:ss"),
|
|
EndTime: moment(item.ActivityDateInserted).format("YYYY-MM-DD HH:mm:ss"),
|
|
PlaybackDuration: item.PlaybackDuration,
|
|
PlayMethod: item.PlayMethod,
|
|
TranscodedVideo: item.TranscodingInfo?.IsVideoDirect || false,
|
|
TranscodedAudio: item.TranscodingInfo?.IsAudioDirect || false,
|
|
ParentId: item.ParentId,
|
|
};
|
|
});
|
|
|
|
let countedstats = countOverlapsPerHour(stats);
|
|
|
|
let hoursRes = {
|
|
types: [
|
|
{ Id: "Transcodes", Name: "Transcodes" },
|
|
{ Id: "DirectPlays", Name: "DirectPlays" },
|
|
],
|
|
|
|
stats: Object.keys(countedstats).map((key) => {
|
|
return {
|
|
Key: key,
|
|
Transcodes: countedstats[key].Transcodes,
|
|
DirectPlays: countedstats[key].DirectPlays,
|
|
};
|
|
}),
|
|
};
|
|
res.send(hoursRes);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getPlaybackMethodStats", async (req, res) => {
|
|
try {
|
|
const { days = 30 } = req.body;
|
|
|
|
if (days < 0) {
|
|
res.status(503);
|
|
return res.send("Days cannot be less than 0");
|
|
}
|
|
|
|
const { rows } = await db.query(
|
|
`select a."PlayMethod" "Name",count(a."PlayMethod") "Count"
|
|
from jf_playback_activity a
|
|
WHERE a."ActivityDateInserted" BETWEEN CURRENT_DATE - MAKE_INTERVAL(days => $1) AND NOW()
|
|
Group by a."PlayMethod"
|
|
ORDER BY (count(*)) DESC;
|
|
`,
|
|
[days - 1]
|
|
);
|
|
|
|
res.send(rows);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.post("/getLibraryLastPlayed", async (req, res) => {
|
|
try {
|
|
const { libraryid } = req.body;
|
|
const { rows } = await db.query(`select * from fs_last_library_activity($1) limit 15`, [libraryid]);
|
|
res.send(rows);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getViewsOverTime", async (req, res) => {
|
|
try {
|
|
const { days } = req.query;
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
const { rows: stats } = await db.query(`select * from fs_watch_stats_over_time($1)`, [_days]);
|
|
|
|
const { rows: libraries } = await db.query(`select distinct "Id","Name" from jf_libraries where archived=false`);
|
|
|
|
const reorganizedData = {};
|
|
|
|
stats.forEach((item) => {
|
|
const library = item.Library;
|
|
const count = item.Count;
|
|
const date = new Date(item.Date).toLocaleDateString("en-US", {
|
|
year: "numeric",
|
|
month: "short",
|
|
day: "2-digit",
|
|
});
|
|
|
|
if (!reorganizedData[date]) {
|
|
reorganizedData[date] = {
|
|
Key: date,
|
|
};
|
|
}
|
|
|
|
reorganizedData[date] = { ...reorganizedData[date], [library]: count };
|
|
});
|
|
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
|
res.send(finalData);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getViewsByDays", async (req, res) => {
|
|
try {
|
|
const { days } = req.query;
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
const { rows: stats } = await db.query(`select * from fs_watch_stats_popular_days_of_week($1)`, [_days]);
|
|
|
|
const { rows: libraries } = await db.query(`select distinct "Id","Name" from jf_libraries where archived=false`);
|
|
|
|
const reorganizedData = {};
|
|
|
|
stats.forEach((item) => {
|
|
const library = item.Library;
|
|
const count = item.Count;
|
|
const day = item.Day;
|
|
|
|
if (!reorganizedData[day]) {
|
|
reorganizedData[day] = {
|
|
Key: day,
|
|
};
|
|
}
|
|
|
|
reorganizedData[day] = { ...reorganizedData[day], [library]: count };
|
|
});
|
|
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
|
res.send(finalData);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getViewsByHour", async (req, res) => {
|
|
try {
|
|
const { days } = req.query;
|
|
let _days = days;
|
|
if (days === undefined) {
|
|
_days = 30;
|
|
}
|
|
const { rows: stats } = await db.query(`select * from fs_watch_stats_popular_hour_of_day($1)`, [_days]);
|
|
|
|
const { rows: libraries } = await db.query(`select distinct "Id","Name" from jf_libraries where archived=false`);
|
|
|
|
const reorganizedData = {};
|
|
|
|
stats.forEach((item) => {
|
|
const library = item.Library;
|
|
const count = item.Count;
|
|
const hour = item.Hour;
|
|
|
|
if (!reorganizedData[hour]) {
|
|
reorganizedData[hour] = {
|
|
Key: hour,
|
|
};
|
|
}
|
|
|
|
reorganizedData[hour] = { ...reorganizedData[hour], [library]: count };
|
|
});
|
|
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
|
res.send(finalData);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getViewsByLibraryType", async (req, res) => {
|
|
try {
|
|
const { days = 30 } = req.query;
|
|
|
|
const { rows } = await db.query(`
|
|
SELECT COALESCE(i."Type", 'Other') AS type, COUNT(a."NowPlayingItemId") AS count
|
|
FROM jf_playback_activity a LEFT JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
|
WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST($1 || ' days' as INTERVAL) AND NOW()
|
|
GROUP BY i."Type"
|
|
`, [days]);
|
|
|
|
const supportedTypes = new Set(["Audio", "Movie", "Series", "Other"]);
|
|
/** @type {Map<string, number>} */
|
|
const reorganizedData = new Map();
|
|
|
|
rows.forEach((item) => {
|
|
const { type, count } = item;
|
|
|
|
if (!supportedTypes.has(type)) return;
|
|
reorganizedData.set(type, count);
|
|
});
|
|
|
|
supportedTypes.forEach((type) => {
|
|
if (reorganizedData.has(type)) return;
|
|
reorganizedData.set(type, 0);
|
|
});
|
|
|
|
res.send(Object.fromEntries(reorganizedData));
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getGenreUserStats", async (req, res) => {
|
|
try {
|
|
const { size = 50, page = 1, userid } = req.query;
|
|
|
|
if (userid === undefined) {
|
|
res.status(400);
|
|
res.send("No User ID provided");
|
|
return;
|
|
}
|
|
|
|
const values = [];
|
|
const query = {
|
|
select: ["COALESCE(g.genre, 'No Genre') AS genre", `SUM(a."PlaybackDuration") AS duration`, "COUNT(*) AS plays"],
|
|
table: "jf_playback_activity_with_metadata",
|
|
alias: "a",
|
|
joins: [
|
|
{
|
|
type: "inner",
|
|
table: "jf_library_items",
|
|
alias: "i",
|
|
conditions: [{ first: "a.NowPlayingItemId", operator: "=", second: "i.Id" }],
|
|
},
|
|
{
|
|
type: "left",
|
|
table: `
|
|
LATERAL (
|
|
SELECT
|
|
jsonb_array_elements_text(
|
|
CASE
|
|
WHEN jsonb_array_length(COALESCE(i."Genres", '[]'::jsonb)) = 0 THEN '["No Genre"]'::jsonb
|
|
ELSE i."Genres"
|
|
END
|
|
) AS genre
|
|
)
|
|
`,
|
|
alias: "g",
|
|
conditions: [{ first: 1, operator: "=", value: 1, wrap: false }],
|
|
},
|
|
],
|
|
|
|
where: [[{ column: "a.UserId", operator: "=", value: `$${values.length + 1}` }]],
|
|
group_by: [`COALESCE(g.genre, 'No Genre')`],
|
|
order_by: "genre",
|
|
sort_order: "asc",
|
|
pageNumber: page,
|
|
pageSize: size,
|
|
};
|
|
|
|
values.push(userid);
|
|
|
|
query.values = values;
|
|
|
|
const result = await dbHelper.query(query);
|
|
|
|
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
|
|
|
|
res.send(response);
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(503);
|
|
res.send(error);
|
|
}
|
|
});
|
|
|
|
router.get("/getGenreLibraryStats", async (req, res) => {
|
|
try {
|
|
const { size = 50, page = 1, libraryid } = req.query;
|
|
|
|
if (libraryid === undefined) {
|
|
res.status(400);
|
|
res.send("No Library ID provided");
|
|
return;
|
|
}
|
|
|
|
const values = [];
|
|
const query = {
|
|
select: ["COALESCE(g.genre, 'No Genre') AS genre", `SUM(a."PlaybackDuration") AS duration`, "COUNT(*) AS plays"],
|
|
table: "jf_playback_activity_with_metadata",
|
|
alias: "a",
|
|
joins: [
|
|
{
|
|
type: "inner",
|
|
table: "jf_library_items",
|
|
alias: "i",
|
|
conditions: [{ first: "a.NowPlayingItemId", operator: "=", second: "i.Id" }],
|
|
},
|
|
{
|
|
type: "left",
|
|
table: `
|
|
LATERAL (
|
|
SELECT
|
|
jsonb_array_elements_text(
|
|
CASE
|
|
WHEN jsonb_array_length(COALESCE(i."Genres", '[]'::jsonb)) = 0 THEN '["No Genre"]'::jsonb
|
|
ELSE i."Genres"
|
|
END
|
|
) AS genre
|
|
)
|
|
`,
|
|
alias: "g",
|
|
conditions: [{ first: 1, operator: "=", value: 1, wrap: false }],
|
|
},
|
|
],
|
|
|
|
where: [[{ column: "a.ParentId", operator: "=", value: `$${values.length + 1}` }]],
|
|
group_by: [`COALESCE(g.genre, 'No Genre')`],
|
|
order_by: "genre",
|
|
sort_order: "asc",
|
|
pageNumber: page,
|
|
pageSize: size,
|
|
};
|
|
|
|
values.push(libraryid);
|
|
|
|
query.values = values;
|
|
|
|
const result = await dbHelper.query(query);
|
|
|
|
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
|
|
|
|
res.send(response);
|
|
} 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" });
|
|
});
|
|
|
|
module.exports = router;
|