Files
Jellystat/backend/routes/api.js
2025-09-29 23:05:31 +02:00

2006 lines
59 KiB
JavaScript

// api.js
const express = require("express");
const db = require("../db");
const dbHelper = require("../classes/db-helper");
const pgp = require("pg-promise")();
const { randomUUID } = require("crypto");
const configClass = require("../classes/config");
const { checkForUpdates } = require("../version-control");
const API = require("../classes/api-loader");
const { sendUpdate } = require("../ws");
const { tables } = require("../global/backup_tables");
const TaskScheduler = require("../classes/task-scheduler-singleton");
const TaskManager = require("../classes/task-manager-singleton.js");
const dayjs = require("dayjs");
const customParseFormat = require("dayjs/plugin/customParseFormat");
dayjs.extend(customParseFormat);
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)` },
{ field: "PlayMethod", column: "a.PlayMethod" },
];
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" },
{ field: "PlayMethod", column: "a.PlayMethod" },
];
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)` },
{ field: "PlayMethod", column: `LOWER(a."PlayMethod")` },
];
//Functions
function groupRecentlyAdded(rows) {
const groupedResults = {};
rows.forEach((row) => {
if (row.Type != "Movie") {
const key = row.SeriesId + row.SeasonId;
if (groupedResults[key]) {
groupedResults[key].NewEpisodeCount++;
} else {
groupedResults[key] = { ...row };
if (row.Type != "Series" && row.Type != "Movie") {
groupedResults[key].NewEpisodeCount = 1;
}
}
} else {
groupedResults[row.Id] = {
...row,
};
}
});
return Object.values(groupedResults);
}
async function purgeLibraryItems(id, withActivity, purgeAll = false) {
let items_query = `select * from jf_library_items where "ParentId"=$1`;
const { rows: items } = await db.query(items_query, [id]);
let seasonIds = [];
let episodeIds = [];
for (const item of items) {
let season_query = `select * from jf_library_seasons where "SeriesId"=$1`;
if (!item.archived && !purgeAll) {
season_query += " and archived=true";
}
const { rows: seasons } = await db.query(season_query, [item.Id]);
seasonIds.push(...seasons.map((item) => item.Id));
if (seasons.length > 0) {
for (const season of seasons) {
let episode_query = `select * from jf_library_episodes where "SeasonId"=$1`;
if (!item.archived && !season.archived && !purgeAll) {
episode_query += " and archived=true";
}
const { rows: episodes } = await db.query(episode_query, [season.Id]);
episodeIds.push(...episodes.map((item) => item.Id));
}
} else {
let episode_query = `select * from jf_library_episodes where "SeriesId"=$1`;
if (!item.archived && !purgeAll) {
episode_query += " and archived=true";
}
const { rows: episodes } = await db.query(episode_query, [item.Id]);
episodeIds.push(...episodes.map((item) => item.Id));
}
}
if (episodeIds.length > 0) {
await db.deleteBulk("jf_library_episodes", episodeIds);
}
if (seasonIds.length > 0) {
await db.deleteBulk("jf_library_seasons", seasonIds);
}
items_query = items_query.replace("select *", "delete");
if (!purgeAll) {
items_query += ` and archived=true`;
}
await db.query(items_query, [id]);
if (withActivity) {
const deleteQuery = {
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();
if (config.error) {
res.status(503);
res.send({ error: config.error });
return;
}
const payload = {
JF_HOST: config.JF_HOST,
APP_USER: config.APP_USER,
settings: config.settings,
REQUIRE_LOGIN: config.REQUIRE_LOGIN,
IS_JELLYFIN: config.IS_JELLYFIN,
};
res.send(payload);
} catch (error) {
console.log(error);
}
});
router.get("/getLibraries", async (req, res) => {
try {
const libraries = await db.query("SELECT * FROM jf_libraries").then((res) => res.rows);
res.send(libraries);
} catch (error) {
res.status(503);
res.send(error);
}
});
router.get("/getRecentlyAdded", async (req, res) => {
try {
const { libraryid, limit = 50, GroupResults = true } = req.query;
const config = await new configClass().getConfig();
const excluded_libraries = config.settings.ExcludedLibraries || [];
let recentlyAddedFromJellystat = await API.getRecentlyAdded({ libraryid: libraryid });
let recentlyAddedFromJellystatMapped = recentlyAddedFromJellystat.map((item) => {
return {
Name: item.Name,
SeriesName: item.SeriesName,
Id: item.Id,
SeriesId: item.SeriesId || null,
SeasonId: item.SeasonId || null,
EpisodeId: item.Type === "Episode" ? item.Id : null,
SeasonNumber: item.ParentIndexNumber ?? null,
EpisodeNumber: item.IndexNumber ?? null,
PrimaryImageHash:
item.ImageTags &&
item.ImageTags.Primary &&
item.ImageBlurHashes &&
item.ImageBlurHashes.Primary &&
item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
: null,
DateCreated: item.DateCreated ?? null,
Type: item.Type,
};
});
if (libraryid !== undefined) {
const { rows: items } = await db.query(
`SELECT i."Name", null "SeriesName", "Id", null "SeriesId", null "SeasonId", null "EpisodeId", null "SeasonNumber", null "EpisodeNumber", "PrimaryImageHash",i."DateCreated", "Type", i."ParentId"
FROM public.jf_library_items i
where i.archived=false
and i."Type" != 'Series'
and i."ParentId"=$1
order by "DateCreated" desc
limit $2`,
[libraryid, limit]
);
const { rows: episodes } = await db.query(
`
SELECT e."Name", e."SeriesName",e."Id" , e."SeriesId", e."SeasonId", e."EpisodeId", e."ParentIndexNumber" "SeasonNumber", e."IndexNumber" "EpisodeNumber", e."PrimaryImageHash", e."DateCreated", e."Type", i."ParentId"
FROM public.jf_library_episodes e
JOIN public.jf_library_items i
on i."Id"=e."SeriesId"
where e."DateCreated" is not null
and e.archived=false
and i."ParentId"=$1
order by e."DateCreated" desc
limit $2`,
[libraryid, limit]
);
let lastSynctedItemDate;
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
lastSynctedItemDate = dayjs(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
}
if (episodes.length > 0 && episodes[0].DateCreated !== undefined && episodes[0].DateCreated !== null) {
const newLastSynctedItemDate = dayjs(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
lastSynctedItemDate = newLastSynctedItemDate;
}
}
if (lastSynctedItemDate !== undefined) {
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
dayjs(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
);
}
const filteredDbRows = [
...items.filter((item) => !excluded_libraries.includes(item.ParentId)),
...episodes.filter((item) => !excluded_libraries.includes(item.ParentId)),
];
const recentlyAdded = [...recentlyAddedFromJellystatMapped, ...filteredDbRows];
// Sort recentlyAdded by DateCreated in descending order
recentlyAdded.sort(
(a, b) => dayjs(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - dayjs(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
);
res.send(recentlyAdded);
return;
}
const { rows: items } = await db.query(
`SELECT i."Name", null "SeriesName", "Id", null "SeriesId", null "SeasonId", null "EpisodeId", null "SeasonNumber" , null "EpisodeNumber" , "PrimaryImageHash",i."DateCreated", "Type", i."ParentId"
FROM public.jf_library_items i
where i.archived=false
order by "DateCreated" desc
limit $1`,
[limit]
);
const { rows: episodes } = await db.query(
`
SELECT e."Name", e."SeriesName",e."Id" , e."SeriesId", e."SeasonId", e."EpisodeId", e."ParentIndexNumber" "SeasonNumber", e."IndexNumber" "EpisodeNumber", e."PrimaryImageHash", e."DateCreated", e."Type", i."ParentId"
FROM public.jf_library_episodes e
JOIN public.jf_library_items i
on i."Id"=e."SeriesId"
where e."DateCreated" is not null
and e.archived=false
order by e."DateCreated" desc
limit $1`,
[limit]
);
let lastSynctedItemDate;
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
lastSynctedItemDate = dayjs(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
}
if (episodes.length > 0 && episodes[0].DateCreated !== undefined && episodes[0].DateCreated !== null) {
const newLastSynctedItemDate = dayjs(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
lastSynctedItemDate = newLastSynctedItemDate;
}
}
if (lastSynctedItemDate !== undefined) {
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
dayjs(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
);
}
const filteredDbRows = [
...items.filter((item) => !excluded_libraries.includes(item.ParentId)),
...episodes.filter((item) => !excluded_libraries.includes(item.ParentId)),
];
let recentlyAdded = [...recentlyAddedFromJellystatMapped, ...filteredDbRows];
recentlyAdded = recentlyAdded.filter((item) => item.Type !== "Series");
if (GroupResults == true) {
recentlyAdded = groupRecentlyAdded(recentlyAdded);
}
// Sort recentlyAdded by DateCreated in descending order
recentlyAdded.sort(
(a, b) => dayjs(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - dayjs(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
);
res.send(recentlyAdded);
return;
} catch (error) {
res.status(503);
res.send(error);
}
});
router.post("/setconfig", async (req, res) => {
try {
const { JF_HOST, JF_API_KEY } = req.body;
if (JF_HOST === undefined && JF_API_KEY === undefined) {
res.status(400);
res.send("JF_HOST and JF_API_KEY are required for configuration");
return;
}
var url = JF_HOST;
const validation = await API.validateSettings(url, JF_API_KEY);
if (validation.isValid === false) {
res.status(validation.status);
res.send(validation);
return;
}
const { rows: getConfig } = await db.query('SELECT * FROM app_config where "ID"=1');
let query = 'UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1';
if (getConfig.length === 0) {
query = 'INSERT INTO app_config ("ID","JF_HOST","JF_API_KEY","APP_USER","APP_PASSWORD") VALUES (1,$1,$2,null,null)';
}
const { rows } = await db.query(query, [validation.cleanedUrl, JF_API_KEY]);
const systemInfo = await API.systemInfo();
if (systemInfo && systemInfo != {}) {
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
settings.ServerID = systemInfo?.Id || null;
const query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
}
}
const admins = await API.getAdmins(true);
const preferredAdmin = await new configClass().getPreferedAdmin();
if (admins && admins.length > 0 && preferredAdmin && !admins.map((item) => item.Id).includes(preferredAdmin)) {
const newAdmin = admins[0];
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
settings.preferred_admin = { userid: newAdmin.Id, username: newAdmin.Name };
const query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
}
}
res.send(rows);
} catch (error) {
console.log(error);
}
});
router.post("/setExternalUrl", async (req, res) => {
try {
const { ExternalUrl } = req.body;
if (ExternalUrl === undefined) {
res.status(400);
res.send("ExternalUrl is required for configuration");
return;
}
const config = await new configClass().getConfig();
const validation = await API.validateSettings(ExternalUrl, config.JF_API_KEY);
if (validation.isValid === false) {
res.status(validation.status);
res.send(validation);
return;
}
try {
const settings = config.settings || {};
settings.EXTERNAL_URL = ExternalUrl;
const query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
config.settings = settings;
res.send(config);
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
} catch (error) {
console.log(error);
res.status(503);
res.send({ error: "Error: " + error });
}
});
router.post("/setPreferredAdmin", async (req, res) => {
try {
const { userid, username } = req.body;
if (userid === undefined && username === undefined) {
res.status(400);
res.send("A valid userid and username is required for preferred admin");
return;
}
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
settings.preferred_admin = { userid: userid, username: username };
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
res.send("Settings updated succesfully");
} else {
res.status(404);
res.send("Settings not found");
}
} catch (error) {
console.log(error);
}
console.log(`ENDPOINT CALLED: /setconfig: `);
});
router.post("/setRequireLogin", async (req, res) => {
try {
const { REQUIRE_LOGIN } = req.body;
if (REQUIRE_LOGIN === undefined || typeof REQUIRE_LOGIN !== "boolean") {
res.status(400);
res.send("A valid value(true/false) is required for REQUIRE_LOGIN");
return;
}
let query = 'UPDATE app_config SET "REQUIRE_LOGIN"=$1 where "ID"=1';
const { rows } = await db.query(query, [REQUIRE_LOGIN]);
res.send(rows);
} catch (error) {
console.log(error);
}
});
router.post("/updateCredentials", async (req, res) => {
const { username, current_password, new_password } = req.body;
const config = await new configClass().getConfig();
let result = { isValid: true, errorMessage: "" };
if (config.error) {
result = { isValid: false, errorMessage: config.error };
res.status(503);
res.send(result);
return;
}
if (username === undefined && current_password === undefined && new_password === undefined) {
result.isValid = false;
result.errorMessage = "Invalid Parameters";
res.status(400);
res.send(result);
return;
}
if (username !== undefined && username === "") {
result.isValid = false;
result.errorMessage = "Username cannot be empty";
res.status(400);
res.send(result);
return;
}
try {
if (username !== undefined && config.APP_USER !== username) {
await db.query(`UPDATE app_config SET "APP_USER"='${username}' where "ID"=1`);
}
if (current_password === undefined && new_password === undefined) {
res.status(400);
res.send(result);
return;
}
if (config.APP_PASSWORD === current_password) {
if (config.APP_PASSWORD === new_password) {
result.isValid = false;
result.errorMessage = "New Password cannot be the same as Old Password";
} else {
await db.query(
`UPDATE app_config SET "APP_PASSWORD"='${new_password}' where "ID"=1 AND "APP_PASSWORD"='${current_password}' `
);
}
} else {
result.isValid = false;
result.errorMessage = "Old Password is Invalid";
}
} catch (error) {
console.log(error);
result.errorMessage = error;
}
if (!result.isValid) {
res.status(400);
}
res.send(result);
});
router.post("/updatePassword", async (req, res) => {
const { current_password, new_password } = req.body;
let result = { isValid: true, errorMessage: "" };
try {
const { rows } = await db.query(
`SELECT "JF_HOST","JF_API_KEY","APP_USER" FROM app_config where "ID"=1 AND "APP_PASSWORD"='${current_password}' `
);
if (rows && rows.length > 0) {
if (current_password === new_password) {
result.isValid = false;
result.errorMessage = "New Password cannot be the same as Old Password";
} else {
await db.query(
`UPDATE app_config SET "APP_PASSWORD"='${new_password}' where "ID"=1 AND "APP_PASSWORD"='${current_password}' `
);
}
} else {
result.isValid = false;
result.errorMessage = "Old Password is Invalid";
}
} catch (error) {
console.log(error);
result.errorMessage = error;
}
res.send(result);
});
router.get("/TrackedLibraries", async (req, res) => {
const config = await new configClass().getConfig();
if (config.error) {
res.send({ error: config.error });
return;
}
try {
const libraries = await API.getLibraries();
const ExcludedLibraries = config.settings?.ExcludedLibraries || [];
const librariesWithTrackedStatus = libraries.map((items) => ({
...items,
...{ Tracked: !ExcludedLibraries.includes(items.Id) },
}));
res.send(librariesWithTrackedStatus);
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
});
router.post("/setExcludedLibraries", async (req, res) => {
const { libraryID } = req.body;
if (libraryID === undefined) {
res.status(400);
res.send("No Library Id provided");
return;
}
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
let libraries = settings.ExcludedLibraries || [];
if (libraries.includes(libraryID)) {
libraries = libraries.filter((item) => item !== libraryID);
} else {
libraries.push(libraryID);
}
settings.ExcludedLibraries = libraries;
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
res.send("Settings updated succesfully");
} else {
res.status(404);
res.send("Settings not found");
}
});
router.get("/UntrackedUsers", async (req, res) => {
const config = await new configClass().getConfig();
if (config.error) {
res.send({ error: config.error });
return;
}
try {
const ExcludedUsers = config.settings?.ExcludedUsers || [];
res.send(ExcludedUsers);
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
});
router.post("/setUntrackedUsers", async (req, res) => {
const { userId } = req.body;
if (Array.isArray(userId) || userId === undefined) {
res.status(400);
return res.send("No Valid User ID provided");
}
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
let excludedUsers = settings.ExcludedUsers || [];
if (excludedUsers.includes(userId)) {
excludedUsers = excludedUsers.filter((item) => item !== userId);
} else {
excludedUsers.push(userId);
}
settings.ExcludedUsers = excludedUsers;
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
res.send(excludedUsers);
} else {
res.status(404);
res.send("Settings not found");
}
});
router.get("/keys", async (req, res) => {
const config = await new configClass().getConfig();
res.send(config.api_keys || []);
});
router.delete("/keys", async (req, res) => {
const { key } = req.body;
const config = await new configClass().getConfig();
if (!key) {
res.status(400);
res.send({ error: "No API key provided to remove" });
return;
}
const keys = config.api_keys || [];
const keyExists = keys.some((obj) => obj.key === key);
if (keyExists) {
const new_keys_array = keys.filter((obj) => obj.key !== key);
let query = 'UPDATE app_config SET api_keys=$1 where "ID"=1';
await db.query(query, [JSON.stringify(new_keys_array)]);
return res.send("Key removed: " + key);
} else {
res.status(404);
return res.send("API key does not exist");
}
});
router.post("/keys", async (req, res) => {
const { name } = req.body;
if (name === undefined) {
res.status(400);
res.send("Key Name is required to generate a key");
return;
}
const config = await new configClass().getConfig();
if (!name) {
res.status(400);
res.send({ error: "A Name is required to generate a key" });
return;
}
let keys = config.api_keys || [];
const uuid = randomUUID();
const new_key = { name: name, key: uuid };
keys.push(new_key);
let query = 'UPDATE app_config SET api_keys=$1 where "ID"=1';
await db.query(query, [JSON.stringify(keys)]);
res.send(keys);
});
router.get("/getTaskSettings", async (req, res) => {
try {
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
let tasksettings = settings.Tasks || {};
res.send(tasksettings);
} else {
res.status(404);
res.send({ error: "Task Settings Not Found" });
}
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
});
router.post("/setTaskSettings", async (req, res) => {
const { taskname, Interval } = req.body;
if (taskname === undefined || Interval === undefined) {
res.status(400);
res.send("Task Name and Interval are required");
return;
}
if (!Number.isInteger(Interval) && Interval <= 0) {
res.status(400);
res.send("A valid Interval(int) which is > 0 minutes is required");
return;
}
try {
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
if (!settings.Tasks) {
settings.Tasks = {};
}
let tasksettings = settings.Tasks;
if (!tasksettings[taskname]) {
tasksettings[taskname] = {};
}
tasksettings[taskname].Interval = Interval;
settings.Tasks = tasksettings;
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
const taskScheduler = new TaskScheduler().getInstance();
await taskScheduler.updateIntervalsFromDB();
await taskScheduler.getTaskHistory();
res.status(200);
res.send(tasksettings);
} else {
res.status(404);
res.send({ error: "Task Settings Not Found" });
}
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
});
// Get Activity Monitor Polling Settings
router.get("/getActivityMonitorSettings", async (req, res) => {
try {
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
console.log(settings);
const pollingSettings = settings.ActivityMonitorPolling || {
activeSessionsInterval: 1000,
idleInterval: 5000
};
res.send(pollingSettings);
} else {
res.status(404);
res.send({ error: "Settings Not Found" });
}
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
});
// Set Activity Monitor Polling Settings
router.post("/setActivityMonitorSettings", async (req, res) => {
const { activeSessionsInterval, idleInterval } = req.body;
if (activeSessionsInterval === undefined || idleInterval === undefined) {
res.status(400);
res.send("activeSessionsInterval and idleInterval are required");
return;
}
if (!Number.isInteger(activeSessionsInterval) || activeSessionsInterval <= 0) {
res.status(400);
res.send("A valid activeSessionsInterval(int) which is > 0 milliseconds is required");
return;
}
if (!Number.isInteger(idleInterval) || idleInterval <= 0) {
res.status(400);
res.send("A valid idleInterval(int) which is > 0 milliseconds is required");
return;
}
if (activeSessionsInterval > idleInterval) {
res.status(400);
res.send("activeSessionsInterval should be <= idleInterval for optimal performance");
return;
}
try {
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
settings.ActivityMonitorPolling = {
activeSessionsInterval: activeSessionsInterval,
idleInterval: idleInterval
};
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
res.status(200);
res.send(settings.ActivityMonitorPolling);
} else {
res.status(404);
res.send({ error: "Settings Not Found" });
}
} catch (error) {
res.status(503);
res.send({ error: "Error: " + error });
}
});
//Jellystat functions
router.get("/CheckForUpdates", async (req, res) => {
try {
let result = await checkForUpdates();
res.send(result);
} catch (error) {
console.log(error);
}
});
//DB Queries
router.post("/getUserDetails", async (req, res) => {
try {
const { userid } = req.body;
if (userid === undefined) {
res.status(400);
res.send("No User Id provided");
return;
}
const { rows } = await db.query(`select * from jf_users where "Id"='${userid}'`);
res.send(rows[0]);
} catch (error) {
console.log(error);
res.status(503);
res.send(error);
}
});
router.get("/getLibraries", async (req, res) => {
try {
const { rows } = await db.query(`SELECT * FROM jf_libraries`);
res.send(rows);
} catch (error) {
console.log(error);
}
});
router.post("/getLibrary", async (req, res) => {
try {
const { libraryid } = req.body;
if (libraryid === undefined) {
res.status(400);
res.send("No Library Id provided");
return;
}
const { rows } = await db.query(`select * from jf_libraries where "Id"='${libraryid}'`);
res.send(rows[0]);
} catch (error) {
console.log(error);
res.status(503);
res.send(error);
}
});
router.post("/getLibraryItems", async (req, res) => {
try {
const { libraryid } = req.body;
if (libraryid === undefined) {
res.status(400);
res.send("No Library Id provided");
return;
}
const { rows } = await db.query(`SELECT * FROM jf_library_items where "ParentId"=$1`, [libraryid]);
res.send(rows);
} catch (error) {
console.log(error);
}
});
router.post("/getSeasons", async (req, res) => {
try {
const { Id } = req.body;
if (Id === undefined) {
res.status(400);
res.send("No Season Id provided");
return;
}
const { rows } = await db.query(
`SELECT s.*, i."PrimaryImageHash", (select count(e.*) "Episodes" from jf_library_episodes e where e."SeasonId"=s."Id") ,(select sum(ii."Size") "Size" from jf_library_episodes e join jf_item_info ii on ii."Id"=e."EpisodeId" where e."SeasonId"=s."Id") FROM jf_library_seasons s left join jf_library_items i on i."Id"=s."SeriesId" where "SeriesId"=$1`,
[Id]
);
res.send(rows);
} catch (error) {
console.log(error);
}
});
router.post("/getEpisodes", async (req, res) => {
try {
const { Id } = req.body;
if (Id === undefined) {
res.status(400);
res.send("No Episode Id provided");
return;
}
const { rows } = await db.query(
`SELECT e.*, i."PrimaryImageHash", ii."Size" FROM jf_library_episodes e left join jf_library_items i on i."Id"=e."SeriesId" join jf_item_info ii on ii."Id"=e."EpisodeId" where "SeasonId"=$1`,
[Id]
);
res.send(rows);
} catch (error) {
console.log(error);
}
});
router.post("/getItemDetails", async (req, res) => {
try {
const { Id } = req.body;
if (Id === undefined) {
res.status(400);
res.send("No ID provided");
return;
}
// let query = `SELECT im."Name" "FileName",im.*,i.* FROM jf_library_items i left join jf_item_info im on i."Id" = im."Id" where i."Id"=$1`;
let query = `SELECT im."Name" "FileName",im."Id",im."Path",im."Name",im."Bitrate",im."MediaStreams",im."Type", COALESCE(im."Size" ,(SELECT SUM(im."Size") FROM jf_library_seasons s JOIN jf_library_episodes e on s."Id"=e."SeasonId" JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."SeriesId" = i."Id")) "Size",i.*, (select "Name" from jf_libraries l where l."Id"=i."ParentId") "LibraryName" FROM jf_library_items i left join jf_item_info im on i."Id" = im."Id" where i."Id"=$1`;
let maxActivityQuery = `SELECT MAX("ActivityDateInserted") "LastActivityDate" FROM public.jf_playback_activity`;
let activityCountQuery = `SELECT Count("ActivityDateInserted") "times_played", SUM("PlaybackDuration") "total_play_time" FROM public.jf_playback_activity`;
const { rows: items } = await db.query(query, [Id]);
if (items.length === 0) {
// query = `SELECT im."Name" "FileName",im.*,s.*, s.archived, i."PrimaryImageHash" FROM jf_library_seasons s left join jf_item_info im on s."Id" = im."Id" left join jf_library_items i on i."Id"=s."SeriesId" where s."Id"=$1`;
query = `SELECT s."Name", (SELECT SUM(im."Size") FROM jf_library_episodes e JOIN jf_item_info im ON im."Id" = e."EpisodeId" WHERE s."Id" = e."SeasonId") AS "Size", s.*, i."PrimaryImageHash", i."ParentId",(select "Name" from jf_libraries l where l."Id"=i."ParentId") "LibraryName" FROM jf_library_seasons s LEFT JOIN jf_library_items i ON i."Id"=s."SeriesId" WHERE s."Id"=$1`;
const { rows: seasons } = await db.query(query, [Id]);
if (seasons.length === 0) {
query = `SELECT im."Name" "FileName",im.*,e.*, e.archived , i."PrimaryImageHash", i."ParentId",(select "Name" from jf_libraries l where l."Id"=i."ParentId") "LibraryName" FROM jf_library_episodes e join jf_item_info im on e."EpisodeId" = im."Id" left join jf_library_items i on i."Id"=e."SeriesId" where e."EpisodeId"=$1`;
const { rows: episodes } = await db.query(query, [Id]);
if (episodes.length !== 0) {
maxActivityQuery = `${maxActivityQuery} where "EpisodeId"=$1`;
activityCountQuery = `${activityCountQuery} where "EpisodeId"=$1`;
const LastActivityDate = await db.querySingle(maxActivityQuery, [Id]);
const TimesPlayed = await db.querySingle(activityCountQuery, [Id]);
episodes.forEach((episode) => {
episode.LastActivityDate = LastActivityDate.LastActivityDate ?? null;
episode.times_played = TimesPlayed.times_played ?? null;
episode.total_play_time = TimesPlayed.total_play_time ?? null;
});
res.send(episodes);
} else {
res.status(404).send("Item not found");
}
} else {
maxActivityQuery = `${maxActivityQuery} where "SeasonId"=$1`;
activityCountQuery = `${activityCountQuery} where "SeasonId"=$1`;
const LastActivityDate = await db.querySingle(maxActivityQuery, [Id]);
const TimesPlayed = await db.querySingle(activityCountQuery, [Id]);
seasons.forEach((season) => {
season.LastActivityDate = LastActivityDate.LastActivityDate ?? null;
season.times_played = TimesPlayed.times_played ?? null;
season.total_play_time = TimesPlayed.total_play_time ?? null;
});
res.send(seasons);
}
} else {
maxActivityQuery = `${maxActivityQuery} where "NowPlayingItemId"=$1`;
activityCountQuery = `${activityCountQuery} where "NowPlayingItemId"=$1`;
const LastActivityDate = await db.querySingle(maxActivityQuery, [Id]);
const TimesPlayed = await db.querySingle(activityCountQuery, [Id]);
items.forEach((item) => {
item.LastActivityDate = LastActivityDate.LastActivityDate ?? null;
item.times_played = TimesPlayed.times_played ?? null;
item.total_play_time = TimesPlayed.total_play_time ?? null;
});
res.send(items);
}
} catch (error) {
console.log(error);
}
});
router.delete("/item/purge", async (req, res) => {
try {
const { id, withActivity } = req.body;
if (id === undefined) {
res.status(400);
res.send("No Item ID provided");
return;
}
const { rows: items } = await db.query(`select * from jf_library_items where "Id"=$1`, [id]);
const { rows: seasons } = await db.query(`select * from jf_library_seasons where "SeriesId"=$1 or "Id"=$1`, [id]);
if (seasons.length > 0) {
for (const season of seasons) {
let delete_season_episodes_query = 'delete from jf_library_episodes where "SeasonId"=$1';
if (!season.archived && (items.length > 0 ? !items[0].archived : true)) {
delete_season_episodes_query += " and archived=true";
}
await db.query(delete_season_episodes_query, [season.Id]);
if (season.archived || (items.length > 0 && items[0].archived)) {
await db.query(`delete from jf_library_seasons where "Id"=$1`, [season.Id]);
}
}
} else {
const { rows: episodes } = await db.query(`select * from jf_library_episodes where "EpisodeId"=$1 and archived=true`, [id]);
if (episodes.length > 0) {
await db.query(`delete from jf_library_episodes where "EpisodeId"=$1 and archived=true`, [id]);
}
if (items.length > 0 && items[0].archived) {
await db.query(`delete from jf_library_episodes where "SeriesId"=$1`, [id]);
await db.query(`delete from jf_library_seasons where "SeriesId"=$1`, [id]);
await db.query(`delete from jf_library_items where "Id"=$1`, [id]);
}
if (withActivity) {
const deleteQuery = {
text: `DELETE FROM jf_playback_activity WHERE${
episodes.length > 0 ? ` "EpisodeId" IN (${pgp.as.csv(episodes.map((item) => item.EpisodeId))}) OR` : ""
}${
seasons.length > 0 ? ` "SeasonId" IN (${pgp.as.csv(seasons.map((item) => item.SeasonId))}) OR` : ""
} "NowPlayingItemId"='${id}'`,
refreshViews: true,
};
await db.query(deleteQuery);
}
}
sendUpdate("GeneralAlert", {
type: "Success",
message: `Item ${withActivity ? "with Playback Activity" : ""} has been Purged`,
});
res.send("Item purged succesfully");
} catch (error) {
console.log(error);
sendUpdate("GeneralAlert", { type: "Error", message: `There was an error Purging the Data` });
res.status(503);
res.send(error);
}
});
router.delete("/library/purge", async (req, res) => {
try {
const { id, withActivity } = req.body;
if (id === undefined) {
res.status(400);
res.send("No Library ID provided");
return;
}
await purgeLibraryItems(id, withActivity, true);
await db.query(`delete from jf_libraries where "Id"=$1`, [id]);
sendUpdate("GeneralAlert", {
type: "Success",
message: `Library ${withActivity ? "with Playback Activity" : ""} has been Purged`,
});
res.send("Item purged succesfully");
} catch (error) {
console.log(error);
sendUpdate("GeneralAlert", { type: "Error", message: `There was an error Purging the Data` });
res.status(503);
res.send(error);
}
});
router.delete("/libraryItems/purge", async (req, res) => {
try {
const { id, withActivity } = req.body;
if (id === undefined) {
res.status(400);
res.send("No Library ID provided");
return;
}
await purgeLibraryItems(id, withActivity);
sendUpdate("GeneralAlert", {
type: "Success",
message: `Library Items ${withActivity ? "with Playback Activity" : ""} has been Purged`,
});
res.send("Item purged succesfully");
} catch (error) {
console.log(error);
sendUpdate("GeneralAlert", { type: "Error", message: `There was an error Purging the Data` });
res.status(503);
res.send(error);
}
});
router.get("/getBackupTables", async (req, res) => {
try {
const config = await new configClass().getConfig();
const excluded_tables = config.settings.ExcludedTables || [];
let backupTables = tables.map((table) => {
return {
...table,
Excluded: excluded_tables.includes(table.value),
};
});
res.send(backupTables);
return;
} catch (error) {
res.status(503);
res.send(error);
}
});
router.post("/setExcludedBackupTable", async (req, res) => {
const { table } = req.body;
if (table === undefined || tables.map((item) => item.value).indexOf(table) === -1) {
res.status(400);
res.send("Invalid table provided");
return;
}
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if (settingsjson.length > 0) {
const settings = settingsjson[0].settings || {};
let excludedTables = settings.ExcludedTables || [];
if (excludedTables.includes(table)) {
excludedTables = excludedTables.filter((item) => item !== table);
} else {
excludedTables.push(table);
}
settings.ExcludedTables = excludedTables;
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
let backupTables = tables.map((table) => {
return {
...table,
Excluded: settings.ExcludedTables.includes(table.value),
};
});
res.send(backupTables);
} else {
res.status(404);
res.send("Settings not found");
}
});
//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 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: "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,
};
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);
}
});
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) {
res.status(400);
res.send("No Library ID provided");
return;
}
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);
res.send(error);
}
});
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) {
res.status(400);
res.send("No Item ID provided");
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 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,
};
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);
res.send(error);
}
});
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) {
res.status(400);
res.send("No User ID provided");
return;
}
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.UserId", operator: "=", value: `$${values.length + 1}` }]],
order_by: sortField,
sort_order: desc ? "desc" : "asc",
pageNumber: page,
pageSize: size,
};
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);
res.send(error);
}
});
router.post("/deletePlaybackActivity", async (req, res) => {
try {
const { ids } = req.body;
if (ids === undefined || !Array.isArray(ids)) {
res.status(400);
res.send("A list of IDs is required. EG: [1,2,3]");
return;
}
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);
res.status(503);
res.send(error);
}
});
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);
}
});
//Tasks
router.get("/stopTask", async (req, res) => {
const { task } = req.query;
if (task === undefined) {
res.status(400);
res.send("No Task provided");
return;
}
const taskManager = new TaskManager().getInstance();
if (taskManager.taskList[task] === undefined) {
res.status(404);
res.send("Task not found");
return;
}
const _task = taskManager.taskList[task];
if (taskManager.isTaskRunning(_task.name)) {
taskManager.stopTask(_task);
res.send("Task Stopped");
return;
} else {
res.status(400);
res.send("Task is not running");
return;
}
});
// Handle other routes
router.use((req, res) => {
res.status(404).send({ error: "Not Found" });
});
module.exports = router;