mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Merge branch 'main' of https://github.com/BreizhHardware/Jellystat into pr/384
This commit is contained in:
@@ -3,7 +3,7 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const configClass = require("./config");
|
||||
|
||||
const moment = require("moment");
|
||||
const dayjs = require("dayjs");
|
||||
const Logging = require("./logging");
|
||||
|
||||
const taskstate = require("../logging/taskstate");
|
||||
@@ -34,7 +34,7 @@ async function backup(refLog) {
|
||||
if (config.error) {
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed: Failed to get config" });
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ async function backup(refLog) {
|
||||
// Get data from each table and append it to the backup file
|
||||
|
||||
try {
|
||||
let now = moment();
|
||||
let now = dayjs();
|
||||
const backuppath = "./" + backupfolder;
|
||||
|
||||
if (!fs.existsSync(backuppath)) {
|
||||
@@ -61,7 +61,7 @@ async function backup(refLog) {
|
||||
console.error("No write permissions for the folder:", backuppath);
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed: No write permissions for the folder: " + backuppath });
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
@@ -73,18 +73,18 @@ async function backup(refLog) {
|
||||
if (filteredTables.length === 0) {
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed: No tables to backup" });
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// const backupPath = `../backup-data/backup_${now.format('yyyy-MM-DD HH-mm-ss')}.json`;
|
||||
const directoryPath = path.join(__dirname, "..", backupfolder, `backup_${now.format("yyyy-MM-DD HH-mm-ss")}.json`);
|
||||
// const backupPath = `../backup-data/backup_${now.format('YYYY-MM-DD HH-mm-ss')}.json`;
|
||||
const directoryPath = path.join(__dirname, "..", backupfolder, `backup_${now.format("YYYY-MM-DD HH-mm-ss")}.json`);
|
||||
refLog.logData.push({ color: "yellow", Message: "Begin Backup " + directoryPath });
|
||||
const stream = fs.createWriteStream(directoryPath, { flags: "a" });
|
||||
stream.on("error", (error) => {
|
||||
stream.on("error", async (error) => {
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed: " + error });
|
||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
return;
|
||||
});
|
||||
const backup_data = [];
|
||||
@@ -152,7 +152,7 @@ async function backup(refLog) {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
refLog.logData.push({ color: "red", Message: "Backup Failed: " + error });
|
||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
|
||||
@@ -104,8 +104,8 @@ class EmbyAPI {
|
||||
|
||||
//Functions
|
||||
|
||||
async getUsers() {
|
||||
if (!this.configReady) {
|
||||
async getUsers(refreshConfig = false) {
|
||||
if (!this.configReady || refreshConfig) {
|
||||
const success = await this.#fetchConfig();
|
||||
if (!success) {
|
||||
return [];
|
||||
@@ -133,9 +133,9 @@ class EmbyAPI {
|
||||
}
|
||||
}
|
||||
|
||||
async getAdmins() {
|
||||
async getAdmins(refreshConfig = false) {
|
||||
try {
|
||||
const users = await this.getUsers();
|
||||
const users = await this.getUsers(refreshConfig);
|
||||
return users?.filter((user) => user.Policy.IsAdministrator) || [];
|
||||
} catch (error) {
|
||||
this.#errorHandler(error);
|
||||
|
||||
@@ -105,8 +105,8 @@ class JellyfinAPI {
|
||||
|
||||
//Functions
|
||||
|
||||
async getUsers() {
|
||||
if (!this.configReady) {
|
||||
async getUsers(refreshConfig = false) {
|
||||
if (!this.configReady || refreshConfig) {
|
||||
const success = await this.#fetchConfig();
|
||||
if (!success) {
|
||||
return [];
|
||||
@@ -133,9 +133,9 @@ class JellyfinAPI {
|
||||
}
|
||||
}
|
||||
|
||||
async getAdmins() {
|
||||
async getAdmins(refreshConfig = false) {
|
||||
try {
|
||||
const users = await this.getUsers();
|
||||
const users = await this.getUsers(refreshConfig);
|
||||
return users?.filter((user) => user.Policy.IsAdministrator) || [];
|
||||
} catch (error) {
|
||||
this.#errorHandler(error);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const db = require("../db");
|
||||
const moment = require("moment");
|
||||
const dayjs = require("dayjs");
|
||||
const taskstate = require("../logging/taskstate");
|
||||
|
||||
const { jf_logging_columns, jf_logging_mapping } = require("../models/jf_logging");
|
||||
|
||||
async function insertLog(uuid, triggertype, taskType) {
|
||||
try {
|
||||
let startTime = moment();
|
||||
let startTime = dayjs();
|
||||
const log = {
|
||||
Id: uuid,
|
||||
Name: taskType,
|
||||
@@ -32,8 +32,8 @@ async function updateLog(uuid, data, taskstate) {
|
||||
if (task.length === 0) {
|
||||
console.log("Unable to find task to update");
|
||||
} else {
|
||||
let endtime = moment();
|
||||
let startTime = moment(task[0].TimeRun);
|
||||
let endtime = dayjs();
|
||||
let startTime = dayjs(task[0].TimeRun);
|
||||
let duration = endtime.diff(startTime, "seconds");
|
||||
const log = {
|
||||
Id: uuid,
|
||||
|
||||
@@ -45,7 +45,7 @@ class TaskManager {
|
||||
if (code !== 0) {
|
||||
console.error(`Worker ${task.name} stopped with exit code ${code}`);
|
||||
}
|
||||
if (onExit) {
|
||||
if (code !== 0 && onExit) {
|
||||
onExit();
|
||||
}
|
||||
delete this.tasks[task.name];
|
||||
|
||||
@@ -19,8 +19,12 @@ class WebhookManager {
|
||||
await this.triggerEventWebhooks('playback_started', data);
|
||||
});
|
||||
|
||||
this.eventEmitter.on('user_login', async (data) => {
|
||||
await this.triggerEventWebhooks('user_login', data);
|
||||
this.eventEmitter.on('playback_ended', async (data) => {
|
||||
await this.triggerEventWebhooks('playback_ended', data);
|
||||
});
|
||||
|
||||
this.eventEmitter.on('media_recently_added', async (data) => {
|
||||
await this.triggerEventWebhooks('media_recently_added', data);
|
||||
});
|
||||
|
||||
// If needed, add more event listeners here
|
||||
@@ -40,11 +44,33 @@ class WebhookManager {
|
||||
).then(res => res.rows);
|
||||
}
|
||||
|
||||
async triggerEventWebhooks(eventType, data) {
|
||||
const webhooks = await this.getWebhooksByEventType(eventType);
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
await this.executeWebhook(webhook, data);
|
||||
async triggerEventWebhooks(eventType, data = {}) {
|
||||
try {
|
||||
const webhooks = await this.getWebhooksByEventType(eventType);
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
|
||||
|
||||
const enrichedData = {
|
||||
...data,
|
||||
event: eventType,
|
||||
triggeredAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const promises = webhooks.map(webhook => {
|
||||
return this.executeWebhook(webhook, enrichedData);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +161,31 @@ class WebhookManager {
|
||||
return template;
|
||||
}
|
||||
|
||||
async triggerEvent(eventType, eventData = {}) {
|
||||
try {
|
||||
const webhooks = this.eventWebhooks?.[eventType] || [];
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
|
||||
|
||||
const promises = webhooks.map(webhook => {
|
||||
return this.webhookManager.executeWebhook(webhook, {
|
||||
...eventData,
|
||||
event: eventType,
|
||||
triggeredAt: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(eventType, data) {
|
||||
this.eventEmitter.emit(eventType, data);
|
||||
}
|
||||
@@ -340,6 +391,28 @@ class WebhookManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async executeDiscordWebhook(webhook, data) {
|
||||
try {
|
||||
console.log(`Execution of discord webhook: ${webhook.name}`);
|
||||
|
||||
const response = await axios.post(webhook.url, data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[WEBHOOK] Discord response: ${response.status}`);
|
||||
return response.status >= 200 && response.status < 300;
|
||||
} catch (error) {
|
||||
console.error(`[WEBHOOK] Error with Discord webhook ${webhook.name}:`, error.message);
|
||||
if (error.response) {
|
||||
console.error('[WEBHOOK] Response status:', error.response.status);
|
||||
console.error('[WEBHOOK] Response data:', error.response.data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebhookManager;
|
||||
@@ -35,6 +35,54 @@ class WebhookScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
async loadEventWebhooks() {
|
||||
try {
|
||||
const eventWebhooks = await this.webhookManager.getEventWebhooks();
|
||||
if (eventWebhooks && eventWebhooks.length > 0) {
|
||||
this.eventWebhooks = {};
|
||||
|
||||
eventWebhooks.forEach(webhook => {
|
||||
if (!this.eventWebhooks[webhook.eventType]) {
|
||||
this.eventWebhooks[webhook.eventType] = [];
|
||||
}
|
||||
this.eventWebhooks[webhook.eventType].push(webhook);
|
||||
});
|
||||
|
||||
console.log(`[WEBHOOK] Loaded ${eventWebhooks.length} event-based webhooks`);
|
||||
} else {
|
||||
console.log('[WEBHOOK] No event-based webhooks found');
|
||||
this.eventWebhooks = {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WEBHOOK] Failed to load event-based webhooks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async triggerEvent(eventType, eventData = {}) {
|
||||
try {
|
||||
const webhooks = this.eventWebhooks[eventType] || [];
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
|
||||
|
||||
const promises = webhooks.map(webhook => {
|
||||
return this.webhookManager.executeWebhook(webhook, {
|
||||
event: eventType,
|
||||
data: eventData,
|
||||
triggeredAt: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleWebhook(webhook) {
|
||||
try {
|
||||
this.cronJobs[webhook.id] = cron.schedule(webhook.schedule, async () => {
|
||||
@@ -50,6 +98,7 @@ class WebhookScheduler {
|
||||
|
||||
async refreshSchedule() {
|
||||
await this.loadScheduledWebhooks();
|
||||
await this.loadEventWebhooks();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,16 @@ const _POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD;
|
||||
const _POSTGRES_IP = process.env.POSTGRES_IP;
|
||||
const _POSTGRES_PORT = process.env.POSTGRES_PORT;
|
||||
const _POSTGRES_DATABASE = process.env.POSTGRES_DB || 'jfstat';
|
||||
const _POSTGRES_SSL_REJECT_UNAUTHORIZED = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
|
||||
|
||||
const client = new Client({
|
||||
host: _POSTGRES_IP,
|
||||
user: _POSTGRES_USER,
|
||||
password: _POSTGRES_PASSWORD,
|
||||
port: _POSTGRES_PORT,
|
||||
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||
? { ssl: { rejectUnauthorized: _POSTGRES_SSL_REJECT_UNAUTHORIZED } }
|
||||
: {})
|
||||
});
|
||||
|
||||
const createDatabase = async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ const _POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD;
|
||||
const _POSTGRES_IP = process.env.POSTGRES_IP;
|
||||
const _POSTGRES_PORT = process.env.POSTGRES_PORT;
|
||||
const _POSTGRES_DATABASE = process.env.POSTGRES_DB || "jfstat";
|
||||
const _POSTGRES_SSL_REJECT_UNAUTHORIZED = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
|
||||
|
||||
if ([_POSTGRES_USER, _POSTGRES_PASSWORD, _POSTGRES_IP, _POSTGRES_PORT].includes(undefined)) {
|
||||
console.log("Error: Postgres details not defined");
|
||||
@@ -22,6 +23,9 @@ const pool = new Pool({
|
||||
max: 20, // Maximum number of connections in the pool
|
||||
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
|
||||
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
|
||||
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||
? { ssl: { rejectUnauthorized: _POSTGRES_SSL_REJECT_UNAUTHORIZED } }
|
||||
: {})
|
||||
});
|
||||
|
||||
pool.on("error", (err, client) => {
|
||||
|
||||
@@ -12,6 +12,9 @@ module.exports = {
|
||||
port:process.env.POSTGRES_PORT,
|
||||
database: process.env.POSTGRES_DB || 'jfstat',
|
||||
createDatabase: true,
|
||||
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||
? { ssl: { rejectUnauthorized: process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true" } }
|
||||
: {})
|
||||
},
|
||||
migrations: {
|
||||
directory: __dirname + '/migrations',
|
||||
@@ -39,6 +42,9 @@ module.exports = {
|
||||
port:process.env.POSTGRES_PORT,
|
||||
database: process.env.POSTGRES_DB || 'jfstat',
|
||||
createDatabase: true,
|
||||
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||
? { ssl: { rejectUnauthorized: process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true" } }
|
||||
: {})
|
||||
},
|
||||
migrations: {
|
||||
directory: __dirname + '/migrations',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const moment = require("moment");
|
||||
const dayjs = require("dayjs");
|
||||
const { randomUUID } = require("crypto");
|
||||
|
||||
const jf_activity_watchdog_columns = [
|
||||
@@ -45,7 +45,7 @@ const jf_activity_watchdog_mapping = (item) => ({
|
||||
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration : 0,
|
||||
PlayMethod: item.PlayState.PlayMethod,
|
||||
ActivityDateInserted:
|
||||
item.ActivityDateInserted !== undefined ? item.ActivityDateInserted : moment().format("YYYY-MM-DD HH:mm:ss.SSSZ"),
|
||||
item.ActivityDateInserted !== undefined ? item.ActivityDateInserted : dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ"),
|
||||
MediaStreams: item.NowPlayingItem.MediaStreams ? item.NowPlayingItem.MediaStreams : null,
|
||||
TranscodingInfo: item.TranscodingInfo ? item.TranscodingInfo : null,
|
||||
PlayState: item.PlayState ? item.PlayState : null,
|
||||
|
||||
@@ -50,7 +50,7 @@ const jf_library_items_mapping = (item) => ({
|
||||
? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
|
||||
: null,
|
||||
archived: false,
|
||||
Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(filterInvalidGenres(item.Genres.map(titleCase))) : [],
|
||||
Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(item.Genres.map(titleCase)) : [],
|
||||
});
|
||||
|
||||
// Utility function to title-case a string
|
||||
@@ -62,53 +62,6 @@ function titleCase(str) {
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function filterInvalidGenres(genres) {
|
||||
const validGenres = [
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Animated",
|
||||
"Biography",
|
||||
"Comedy",
|
||||
"Crime",
|
||||
"Dance",
|
||||
"Disaster",
|
||||
"Documentary",
|
||||
"Drama",
|
||||
"Erotic",
|
||||
"Family",
|
||||
"Fantasy",
|
||||
"Found Footage",
|
||||
"Historical",
|
||||
"Horror",
|
||||
"Independent",
|
||||
"Legal",
|
||||
"Live Action",
|
||||
"Martial Arts",
|
||||
"Musical",
|
||||
"Mystery",
|
||||
"Noir",
|
||||
"Performance",
|
||||
"Political",
|
||||
"Romance",
|
||||
"Satire",
|
||||
"Science Fiction",
|
||||
"Short",
|
||||
"Silent",
|
||||
"Slasher",
|
||||
"Sports",
|
||||
"Spy",
|
||||
"Superhero",
|
||||
"Supernatural",
|
||||
"Suspense",
|
||||
"Teen",
|
||||
"Thriller",
|
||||
"War",
|
||||
"Western",
|
||||
];
|
||||
|
||||
return genres.filter((genre) => validGenres.map((g) => g.toLowerCase()).includes(genre.toLowerCase()));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
jf_library_items_columns,
|
||||
jf_library_items_mapping,
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
////////////////////////// pn delete move to playback
|
||||
const columnsPlaybackReporting = [
|
||||
"rowid",
|
||||
"DateCreated",
|
||||
"UserId",
|
||||
"ItemId",
|
||||
"ItemType",
|
||||
"ItemName",
|
||||
"PlaybackMethod",
|
||||
"ClientName",
|
||||
"DeviceName",
|
||||
"PlayDuration",
|
||||
];
|
||||
////////////////////////// pn delete move to playback
|
||||
const columnsPlaybackReporting = [
|
||||
"rowid",
|
||||
"DateCreated",
|
||||
"UserId",
|
||||
"ItemId",
|
||||
"ItemType",
|
||||
"ItemName",
|
||||
"PlaybackMethod",
|
||||
"ClientName",
|
||||
"DeviceName",
|
||||
"PlayDuration",
|
||||
];
|
||||
|
||||
const mappingPlaybackReporting = (item) => {
|
||||
let duration = item[9];
|
||||
|
||||
const mappingPlaybackReporting = (item) => ({
|
||||
rowid:item[0] ,
|
||||
DateCreated:item[1] ,
|
||||
UserId:item[2] ,
|
||||
ItemId:item[3] ,
|
||||
ItemType:item[4] ,
|
||||
ItemName:item[5] ,
|
||||
PlaybackMethod:item[6] ,
|
||||
ClientName:item[7] ,
|
||||
DeviceName:item[8] ,
|
||||
PlayDuration:item[9] ,
|
||||
});
|
||||
if (duration === null || duration === undefined || duration < 0) {
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
columnsPlaybackReporting,
|
||||
mappingPlaybackReporting,
|
||||
};
|
||||
return {
|
||||
rowid: item[0],
|
||||
DateCreated: item[1],
|
||||
UserId: item[2],
|
||||
ItemId: item[3],
|
||||
ItemType: item[4],
|
||||
ItemName: item[5],
|
||||
PlaybackMethod: item[6],
|
||||
ClientName: item[7],
|
||||
DeviceName: item[8],
|
||||
PlayDuration: duration,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
columnsPlaybackReporting,
|
||||
mappingPlaybackReporting,
|
||||
};
|
||||
|
||||
@@ -11,11 +11,14 @@ const configClass = require("../classes/config");
|
||||
const { checkForUpdates } = require("../version-control");
|
||||
const API = require("../classes/api-loader");
|
||||
const { sendUpdate } = require("../ws");
|
||||
const moment = require("moment");
|
||||
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
|
||||
@@ -329,11 +332,11 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
|
||||
let lastSynctedItemDate;
|
||||
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
|
||||
lastSynctedItemDate = moment(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
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 = moment(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
const newLastSynctedItemDate = dayjs(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
|
||||
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
|
||||
lastSynctedItemDate = newLastSynctedItemDate;
|
||||
@@ -342,7 +345,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
|
||||
if (lastSynctedItemDate !== undefined) {
|
||||
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
|
||||
moment(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
||||
dayjs(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,7 +357,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
const recentlyAdded = [...recentlyAddedFromJellystatMapped, ...filteredDbRows];
|
||||
// Sort recentlyAdded by DateCreated in descending order
|
||||
recentlyAdded.sort(
|
||||
(a, b) => moment(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - moment(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
|
||||
(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);
|
||||
@@ -383,11 +386,11 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
);
|
||||
let lastSynctedItemDate;
|
||||
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
|
||||
lastSynctedItemDate = moment(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
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 = moment(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
const newLastSynctedItemDate = dayjs(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
|
||||
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
|
||||
lastSynctedItemDate = newLastSynctedItemDate;
|
||||
@@ -396,7 +399,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
|
||||
if (lastSynctedItemDate !== undefined) {
|
||||
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
|
||||
moment(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
||||
dayjs(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -414,7 +417,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
|
||||
// Sort recentlyAdded by DateCreated in descending order
|
||||
recentlyAdded.sort(
|
||||
(a, b) => moment(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - moment(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
|
||||
(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);
|
||||
@@ -463,7 +466,24 @@ router.post("/setconfig", async (req, res) => {
|
||||
|
||||
settings.ServerID = systemInfo?.Id || null;
|
||||
|
||||
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
|
||||
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]);
|
||||
}
|
||||
@@ -892,6 +912,83 @@ router.post("/setTaskSettings", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -20,16 +20,23 @@ router.post("/login", async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password || password === CryptoJS.SHA3("").toString()) {
|
||||
const query = "SELECT * FROM app_config";
|
||||
const { rows: login } = await db.query(query);
|
||||
|
||||
if (
|
||||
(!username || !password || password === CryptoJS.SHA3("").toString()) &&
|
||||
login.length > 0 &&
|
||||
login[0].REQUIRE_LOGIN == true
|
||||
) {
|
||||
res.sendStatus(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const query = 'SELECT * FROM app_config WHERE ("APP_USER" = $1 AND "APP_PASSWORD" = $2) OR "REQUIRE_LOGIN" = false';
|
||||
const values = [username, password];
|
||||
const { rows: login } = await db.query(query, values);
|
||||
const loginUser = login.filter(
|
||||
(user) => (user.APP_USER === username && user.APP_PASSWORD === password) || user.REQUIRE_LOGIN == false
|
||||
);
|
||||
|
||||
if (login.length > 0 || (username === JS_USER && password === CryptoJS.SHA3(JS_PASSWORD).toString())) {
|
||||
if (loginUser.length > 0 || (username === JS_USER && password === CryptoJS.SHA3(JS_PASSWORD).toString())) {
|
||||
const user = { id: 1, username: username };
|
||||
|
||||
jwt.sign({ user }, JWT_SECRET, (err, token) => {
|
||||
|
||||
@@ -23,6 +23,8 @@ const postgresPassword = process.env.POSTGRES_PASSWORD;
|
||||
const postgresIp = process.env.POSTGRES_IP;
|
||||
const postgresPort = process.env.POSTGRES_PORT;
|
||||
const postgresDatabase = process.env.POSTGRES_DB || "jfstat";
|
||||
const postgresSslRejectUnauthorized = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
|
||||
|
||||
const backupfolder = "backup-data";
|
||||
|
||||
// Restore function
|
||||
@@ -52,6 +54,9 @@ async function restore(file, refLog) {
|
||||
host: postgresIp,
|
||||
port: postgresPort,
|
||||
database: postgresDatabase,
|
||||
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||
? { ssl: { rejectUnauthorized: postgresSslRejectUnauthorized } }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const backupPath = file;
|
||||
|
||||
@@ -148,7 +148,7 @@ router.get("/getSessions", async (req, res) => {
|
||||
|
||||
router.get("/getAdminUsers", async (req, res) => {
|
||||
try {
|
||||
const adminUser = await API.getAdmins();
|
||||
const adminUser = await API.getAdmins(true);
|
||||
res.send(adminUser);
|
||||
} catch (error) {
|
||||
res.status(503);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
const express = require("express");
|
||||
const db = require("../db");
|
||||
const dbHelper = require("../classes/db-helper");
|
||||
const moment = require("moment");
|
||||
|
||||
const dayjs = require("dayjs");
|
||||
const customParseFormat = require("dayjs/plugin/customParseFormat");
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -11,8 +14,8 @@ function countOverlapsPerHour(records) {
|
||||
const hourCounts = {};
|
||||
|
||||
records.forEach((record) => {
|
||||
const start = moment(record.StartTime).subtract(1, "hour");
|
||||
const end = moment(record.EndTime).add(1, "hour");
|
||||
const start = dayjs(record.StartTime).subtract(1, "hour");
|
||||
const end = dayjs(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")) {
|
||||
@@ -289,12 +292,12 @@ router.post("/getLibraryItemsWithStats", async (req, res) => {
|
||||
|
||||
router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
||||
try {
|
||||
let { libraryid, startDate, endDate = moment(), hours = 24 } = req.body;
|
||||
let { libraryid, startDate, endDate = dayjs(), hours = 24 } = req.body;
|
||||
|
||||
// Validate startDate and endDate using moment
|
||||
// Validate startDate and endDate using dayjs
|
||||
if (
|
||||
startDate !== undefined &&
|
||||
(!moment(startDate, moment.ISO_8601, true).isValid() || !moment(endDate, moment.ISO_8601, true).isValid())
|
||||
(!dayjs(startDate, "YYYY-MM-DDTHH:mm:ss.SSSZ", true).isValid() || !dayjs(endDate, "YYYY-MM-DDTHH:mm:ss.SSSZ", true).isValid())
|
||||
) {
|
||||
return res.status(400).send({ error: "Invalid date format" });
|
||||
}
|
||||
@@ -308,7 +311,7 @@ router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
||||
}
|
||||
|
||||
if (startDate === undefined) {
|
||||
startDate = moment(endDate).subtract(hours, "hour").format("YYYY-MM-DD HH:mm:ss");
|
||||
startDate = dayjs(endDate).subtract(hours, "hour").format("YYYY-MM-DD HH:mm:ss");
|
||||
}
|
||||
|
||||
const { rows } = await db.query(
|
||||
@@ -336,8 +339,8 @@ router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
||||
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"),
|
||||
StartTime: dayjs(item.ActivityDateInserted).subtract(item.PlaybackDuration, "seconds").format("YYYY-MM-DD HH:mm:ss"),
|
||||
EndTime: dayjs(item.ActivityDateInserted).format("YYYY-MM-DD HH:mm:ss"),
|
||||
PlaybackDuration: item.PlaybackDuration,
|
||||
PlayMethod: item.PlayMethod,
|
||||
TranscodedVideo: item.TranscodingInfo?.IsVideoDirect || false,
|
||||
@@ -407,9 +410,9 @@ router.post("/getLibraryLastPlayed", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getViewsOverTime", async (req, res) => {
|
||||
router.get("/getViewsOverTime", async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const { days } = req.query;
|
||||
let _days = days;
|
||||
if (days === undefined) {
|
||||
_days = 30;
|
||||
@@ -423,6 +426,7 @@ router.post("/getViewsOverTime", async (req, res) => {
|
||||
stats.forEach((item) => {
|
||||
const library = item.Library;
|
||||
const count = item.Count;
|
||||
const duration = item.Duration;
|
||||
const date = new Date(item.Date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
@@ -435,7 +439,7 @@ router.post("/getViewsOverTime", async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
reorganizedData[date] = { ...reorganizedData[date], [library]: count };
|
||||
reorganizedData[date] = { ...reorganizedData[date], [library]: { count, duration } };
|
||||
});
|
||||
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
||||
res.send(finalData);
|
||||
@@ -446,9 +450,9 @@ router.post("/getViewsOverTime", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getViewsByDays", async (req, res) => {
|
||||
router.get("/getViewsByDays", async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const { days } = req.query;
|
||||
let _days = days;
|
||||
if (days === undefined) {
|
||||
_days = 30;
|
||||
@@ -462,6 +466,7 @@ router.post("/getViewsByDays", async (req, res) => {
|
||||
stats.forEach((item) => {
|
||||
const library = item.Library;
|
||||
const count = item.Count;
|
||||
const duration = item.Duration;
|
||||
const day = item.Day;
|
||||
|
||||
if (!reorganizedData[day]) {
|
||||
@@ -470,7 +475,7 @@ router.post("/getViewsByDays", async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
reorganizedData[day] = { ...reorganizedData[day], [library]: count };
|
||||
reorganizedData[day] = { ...reorganizedData[day], [library]: { count, duration } };
|
||||
});
|
||||
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
||||
res.send(finalData);
|
||||
@@ -481,9 +486,9 @@ router.post("/getViewsByDays", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getViewsByHour", async (req, res) => {
|
||||
router.get("/getViewsByHour", async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const { days } = req.query;
|
||||
let _days = days;
|
||||
if (days === undefined) {
|
||||
_days = 30;
|
||||
@@ -497,6 +502,7 @@ router.post("/getViewsByHour", async (req, res) => {
|
||||
stats.forEach((item) => {
|
||||
const library = item.Library;
|
||||
const count = item.Count;
|
||||
const duration = item.Duration;
|
||||
const hour = item.Hour;
|
||||
|
||||
if (!reorganizedData[hour]) {
|
||||
@@ -505,7 +511,7 @@ router.post("/getViewsByHour", async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
reorganizedData[hour] = { ...reorganizedData[hour], [library]: count };
|
||||
reorganizedData[hour] = { ...reorganizedData[hour], [library]: { count, duration } };
|
||||
});
|
||||
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
||||
res.send(finalData);
|
||||
@@ -516,6 +522,41 @@ router.post("/getViewsByHour", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require("express");
|
||||
const db = require("../db");
|
||||
|
||||
const moment = require("moment");
|
||||
const dayjs = require("dayjs");
|
||||
const { randomUUID } = require("crypto");
|
||||
|
||||
const { sendUpdate } = require("../ws");
|
||||
@@ -39,13 +39,41 @@ function getErrorLineNumber(error) {
|
||||
return lineNumber;
|
||||
}
|
||||
|
||||
function sanitizeNullBytes(obj) {
|
||||
if (typeof obj === 'string') {
|
||||
// Remove various forms of null bytes and control characters that cause Unicode escape sequence errors
|
||||
return obj
|
||||
.replace(/\u0000/g, '') // Remove null bytes
|
||||
.replace(/\\u0000/g, '') // Remove escaped null bytes
|
||||
.replace(/\x00/g, '') // Remove hex null bytes
|
||||
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Remove all control characters
|
||||
.trim(); // Remove leading/trailing whitespace
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(sanitizeNullBytes);
|
||||
}
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
const sanitized = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
sanitized[key] = sanitizeNullBytes(value);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
class sync {
|
||||
async getExistingIDsforTable(tablename) {
|
||||
return await db.query(`SELECT "Id" FROM ${tablename}`).then((res) => res.rows.map((row) => row.Id));
|
||||
}
|
||||
|
||||
async insertData(tablename, dataToInsert, column_mappings) {
|
||||
let result = await db.insertBulk(tablename, dataToInsert, column_mappings);
|
||||
const sanitizedData = sanitizeNullBytes(dataToInsert);
|
||||
|
||||
let result = await db.insertBulk(tablename, sanitizedData, column_mappings);
|
||||
if (result.Result === "SUCCESS") {
|
||||
// syncTask.loggedData.push({ color: "dodgerblue", Message: dataToInsert.length + " Rows Inserted." });
|
||||
} else {
|
||||
@@ -395,12 +423,13 @@ async function removeOrphanedData() {
|
||||
syncTask.loggedData.push({ color: "yellow", Message: "Removing Orphaned FileInfo/Episode/Season Records" });
|
||||
|
||||
await db.query("CALL jd_remove_orphaned_data()");
|
||||
const archived_items = await db
|
||||
.query(`select "Id" from jf_library_items where archived=true and "Type"='Series'`)
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
const archived_seasons = await db
|
||||
.query(`select "Id" from jf_library_seasons where archived=true`)
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
const archived_items_query = `select i."Id" from jf_library_items i join jf_library_seasons s on s."SeriesId"=i."Id" and s.archived=false where i.archived=true and i."Type"='Series'
|
||||
union
|
||||
select i."Id" from jf_library_items i join jf_library_episodes e on e."SeriesId"=i."Id" and e.archived=false where i.archived=true and i."Type"='Series'
|
||||
`;
|
||||
const archived_items = await db.query(archived_items_query).then((res) => res.rows.map((row) => row.Id));
|
||||
const archived_seasons_query = `select s."Id" from jf_library_seasons s join jf_library_episodes e on e."SeasonId"=s."Id" and e.archived=false where s.archived=true`;
|
||||
const archived_seasons = await db.query(archived_seasons_query).then((res) => res.rows.map((row) => row.Id));
|
||||
if (!(await _sync.updateSingleFieldOnDB("jf_library_seasons", archived_items, "archived", true, "SeriesId"))) {
|
||||
syncTask.loggedData.push({ color: "red", Message: "Error archiving library seasons" });
|
||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||
@@ -529,13 +558,13 @@ async function syncPlaybackPluginData() {
|
||||
let query = `SELECT rowid, * FROM PlaybackActivity`;
|
||||
|
||||
if (OldestPlaybackActivity && NewestPlaybackActivity) {
|
||||
const formattedDateTimeOld = moment(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedDateTimeNew = moment(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedDateTimeOld = dayjs(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedDateTimeNew = dayjs(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
query = query + ` WHERE (DateCreated < '${formattedDateTimeOld}' or DateCreated > '${formattedDateTimeNew}')`;
|
||||
}
|
||||
|
||||
if (OldestPlaybackActivity && !NewestPlaybackActivity) {
|
||||
const formattedDateTimeOld = moment(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedDateTimeOld = dayjs(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
query = query + ` WHERE DateCreated < '${formattedDateTimeOld}'`;
|
||||
if (MaxPlaybackReportingPluginID) {
|
||||
query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
|
||||
@@ -543,7 +572,7 @@ async function syncPlaybackPluginData() {
|
||||
}
|
||||
|
||||
if (!OldestPlaybackActivity && NewestPlaybackActivity) {
|
||||
const formattedDateTimeNew = moment(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedDateTimeNew = dayjs(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||
query = query + ` WHERE DateCreated > '${formattedDateTimeNew}'`;
|
||||
if (MaxPlaybackReportingPluginID) {
|
||||
query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
|
||||
@@ -823,6 +852,8 @@ async function partialSync(triggertype) {
|
||||
const config = await new configClass().getConfig();
|
||||
|
||||
const uuid = randomUUID();
|
||||
|
||||
const newItems = []; // Array to track newly added items during the sync process
|
||||
|
||||
syncTask = { loggedData: [], uuid: uuid, wsKey: "PartialSyncTask", taskName: taskName.partialsync };
|
||||
try {
|
||||
@@ -832,7 +863,7 @@ async function partialSync(triggertype) {
|
||||
if (config.error) {
|
||||
syncTask.loggedData.push({ Message: config.error });
|
||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||
return;
|
||||
return { success: false, error: config.error };
|
||||
}
|
||||
|
||||
const libraries = await API.getLibraries();
|
||||
@@ -841,7 +872,7 @@ async function partialSync(triggertype) {
|
||||
syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." });
|
||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " " + taskName.fullsync + " Completed" });
|
||||
return;
|
||||
return { success: false, error: "No libraries found" };
|
||||
}
|
||||
|
||||
const excluded_libraries = config.settings.ExcludedLibraries || [];
|
||||
@@ -849,10 +880,10 @@ async function partialSync(triggertype) {
|
||||
const filtered_libraries = libraries.filter((library) => !excluded_libraries.includes(library.Id));
|
||||
const existing_excluded_libraries = libraries.filter((library) => excluded_libraries.includes(library.Id));
|
||||
|
||||
// //syncUserData
|
||||
// syncUserData
|
||||
await syncUserData();
|
||||
|
||||
// //syncLibraryFolders
|
||||
// syncLibraryFolders
|
||||
await syncLibraryFolders(filtered_libraries, existing_excluded_libraries);
|
||||
|
||||
//item sync counters
|
||||
@@ -870,7 +901,7 @@ async function partialSync(triggertype) {
|
||||
let updateItemInfoCount = 0;
|
||||
let updateEpisodeInfoCount = 0;
|
||||
|
||||
let lastSyncDate = moment().subtract(24, "hours");
|
||||
let lastSyncDate = dayjs().subtract(24, "hours");
|
||||
|
||||
const last_execution = await db
|
||||
.query(
|
||||
@@ -881,7 +912,7 @@ async function partialSync(triggertype) {
|
||||
)
|
||||
.then((res) => res.rows);
|
||||
if (last_execution.length !== 0) {
|
||||
lastSyncDate = moment(last_execution[0].DateCreated);
|
||||
lastSyncDate = dayjs(last_execution[0].DateCreated);
|
||||
}
|
||||
|
||||
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
|
||||
@@ -908,7 +939,7 @@ async function partialSync(triggertype) {
|
||||
},
|
||||
});
|
||||
|
||||
libraryItems = libraryItems.filter((item) => moment(item.DateCreated).isAfter(lastSyncDate));
|
||||
libraryItems = libraryItems.filter((item) => dayjs(item.DateCreated).isAfter(lastSyncDate));
|
||||
|
||||
while (libraryItems.length != 0) {
|
||||
if (libraryItems.length === 0 && startIndex === 0) {
|
||||
@@ -955,7 +986,7 @@ async function partialSync(triggertype) {
|
||||
insertEpisodeInfoCount += Number(infoCount.insertEpisodeInfoCount);
|
||||
updateEpisodeInfoCount += Number(infoCount.updateEpisodeInfoCount);
|
||||
|
||||
//clear data from memory as its no longer needed
|
||||
//clear data from memory as it's no longer needed
|
||||
library_items = null;
|
||||
seasons = null;
|
||||
episodes = null;
|
||||
@@ -973,7 +1004,7 @@ async function partialSync(triggertype) {
|
||||
},
|
||||
});
|
||||
|
||||
libraryItems = libraryItems.filter((item) => moment(item.DateCreated).isAfter(lastSyncDate));
|
||||
libraryItems = libraryItems.filter((item) => dayjs(item.DateCreated).isAfter(lastSyncDate));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1022,10 +1053,22 @@ async function partialSync(triggertype) {
|
||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.SUCCESS);
|
||||
|
||||
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " Sync Completed" });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newItems: newItems,
|
||||
stats: {
|
||||
itemsAdded: insertedItemsCount,
|
||||
episodesAdded: insertedEpisodeCount,
|
||||
seasonsAdded: insertedSeasonsCount
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
syncTask.loggedData.push({ color: "red", Message: getErrorLineNumber(error) + ": Error: " + error });
|
||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||
sendUpdate(syncTask.wsKey, { type: "Error", message: triggertype + " Sync Halted with Errors" });
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,18 +185,128 @@ router.post('/:id/test', async (req, res) => {
|
||||
}
|
||||
|
||||
const webhook = result.rows[0];
|
||||
const testData = req.body || {};
|
||||
let testData = req.body || {};
|
||||
let success = false;
|
||||
|
||||
const success = await webhookManager.executeWebhook(webhook, testData);
|
||||
// Discord behaviour
|
||||
if (webhook.url.includes('discord.com/api/webhooks')) {
|
||||
console.log('Discord webhook détecté, préparation du payload spécifique');
|
||||
|
||||
// Discord specific format
|
||||
testData = {
|
||||
content: "Test de webhook depuis Jellystat",
|
||||
embeds: [{
|
||||
title: "Discord test notification",
|
||||
description: "This is a test notification of jellystat discord webhook",
|
||||
color: 3447003,
|
||||
fields: [
|
||||
{
|
||||
name: "Webhook type",
|
||||
value: webhook.trigger_type || "Not specified",
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "ID",
|
||||
value: webhook.id,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
timestamp: new Date().toISOString()
|
||||
}]
|
||||
};
|
||||
|
||||
// Bypass classic method for discord
|
||||
success = await webhookManager.executeDiscordWebhook(webhook, testData);
|
||||
}
|
||||
else if (webhook.trigger_type === 'event' && webhook.event_type) {
|
||||
const eventType = webhook.event_type;
|
||||
|
||||
let eventData = {};
|
||||
|
||||
switch (eventType) {
|
||||
case 'playback_started':
|
||||
eventData = {
|
||||
sessionInfo: {
|
||||
userId: "test-user-id",
|
||||
deviceId: "test-device-id",
|
||||
deviceName: "Test Device",
|
||||
clientName: "Test Client",
|
||||
isPaused: false,
|
||||
mediaType: "Movie",
|
||||
mediaName: "Test Movie",
|
||||
startTime: new Date().toISOString()
|
||||
},
|
||||
userData: {
|
||||
username: "Test User",
|
||||
userImageTag: "test-image-tag"
|
||||
},
|
||||
mediaInfo: {
|
||||
itemId: "test-item-id",
|
||||
episodeId: null,
|
||||
mediaName: "Test Movie",
|
||||
seasonName: null,
|
||||
seriesName: null
|
||||
}
|
||||
};
|
||||
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||
break;
|
||||
|
||||
case 'playback_ended':
|
||||
eventData = {
|
||||
sessionInfo: {
|
||||
userId: "test-user-id",
|
||||
deviceId: "test-device-id",
|
||||
deviceName: "Test Device",
|
||||
clientName: "Test Client",
|
||||
mediaType: "Movie",
|
||||
mediaName: "Test Movie",
|
||||
startTime: new Date(Date.now() - 3600000).toISOString(),
|
||||
endTime: new Date().toISOString(),
|
||||
playbackDuration: 3600
|
||||
},
|
||||
userData: {
|
||||
username: "Test User",
|
||||
userImageTag: "test-image-tag"
|
||||
},
|
||||
mediaInfo: {
|
||||
itemId: "test-item-id",
|
||||
episodeId: null,
|
||||
mediaName: "Test Movie",
|
||||
seasonName: null,
|
||||
seriesName: null
|
||||
}
|
||||
};
|
||||
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||
break;
|
||||
|
||||
case 'media_recently_added':
|
||||
eventData = {
|
||||
mediaItem: {
|
||||
id: "test-item-id",
|
||||
name: "Test Media",
|
||||
type: "Movie",
|
||||
overview: "This is a test movie for webhook testing",
|
||||
addedDate: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||
break;
|
||||
|
||||
default:
|
||||
success = await webhookManager.executeWebhook(webhook, testData);
|
||||
}
|
||||
} else {
|
||||
success = await webhookManager.executeWebhook(webhook, testData);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
res.json({ message: 'Webhook executed successfully' });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Webhook execution failed' });
|
||||
res.status(500).json({ error: 'Error while executing webhook' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error testing webhook:', error);
|
||||
res.status(500).json({ error: 'Failed to test webhook' });
|
||||
res.status(500).json({ error: 'Failed to test webhook: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,10 +315,110 @@ router.post('/:id/trigger-monthly', async (req, res) => {
|
||||
const success = await webhookManager.triggerMonthlySummaryWebhook(req.params.id);
|
||||
|
||||
if (success) {
|
||||
res.status(200).json({ message: "Rapport mensuel envoyé avec succès" });
|
||||
res.status(200).json({ message: "Monthly report sent successfully" });
|
||||
} else {
|
||||
res.status(500).json({ message: "Échec de l'envoi du rapport mensuel" });
|
||||
res.status(500).json({ message: "Failed to send monthly report" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
// Get status of event webhooks
|
||||
router.get('/event-status', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const eventTypes = ['playback_started', 'playback_ended', 'media_recently_added'];
|
||||
const result = {};
|
||||
|
||||
for (const eventType of eventTypes) {
|
||||
const webhooks = await dbInstance.query(
|
||||
'SELECT id, name, enabled FROM webhooks WHERE trigger_type = $1 AND event_type = $2',
|
||||
['event', eventType]
|
||||
);
|
||||
|
||||
result[eventType] = {
|
||||
exists: webhooks.rows.length > 0,
|
||||
enabled: webhooks.rows.some(webhook => webhook.enabled),
|
||||
webhooks: webhooks.rows
|
||||
};
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Error fetching webhook status:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch webhook status' });
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle all webhooks of a specific event type
|
||||
router.post('/toggle-event/:eventType', async (req, res) => {
|
||||
try {
|
||||
const { eventType } = req.params;
|
||||
const { enabled } = req.body;
|
||||
|
||||
if (!['playback_started', 'playback_ended', 'media_recently_added'].includes(eventType)) {
|
||||
return res.status(400).json({ error: 'Invalid event type' });
|
||||
}
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
return res.status(400).json({ error: 'Enabled parameter must be a boolean' });
|
||||
}
|
||||
|
||||
// Mettre à jour tous les webhooks de ce type d'événement
|
||||
const result = await dbInstance.query(
|
||||
'UPDATE webhooks SET enabled = $1 WHERE trigger_type = $2 AND event_type = $3 RETURNING id',
|
||||
[enabled, 'event', eventType]
|
||||
);
|
||||
|
||||
// Si aucun webhook n'existe pour ce type, en créer un de base
|
||||
if (result.rows.length === 0 && enabled) {
|
||||
const defaultWebhook = {
|
||||
name: `Webhook pour ${eventType}`,
|
||||
url: req.body.url || '',
|
||||
method: 'POST',
|
||||
trigger_type: 'event',
|
||||
event_type: eventType,
|
||||
enabled: true,
|
||||
headers: '{}',
|
||||
payload: JSON.stringify({
|
||||
event: `{{event}}`,
|
||||
data: `{{data}}`,
|
||||
timestamp: `{{triggeredAt}}`
|
||||
})
|
||||
};
|
||||
|
||||
if (!defaultWebhook.url) {
|
||||
return res.status(400).json({
|
||||
error: 'URL parameter is required when creating a new webhook',
|
||||
needsUrl: true
|
||||
});
|
||||
}
|
||||
|
||||
await dbInstance.query(
|
||||
`INSERT INTO webhooks (name, url, method, trigger_type, event_type, enabled, headers, payload)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
[
|
||||
defaultWebhook.name,
|
||||
defaultWebhook.url,
|
||||
defaultWebhook.method,
|
||||
defaultWebhook.trigger_type,
|
||||
defaultWebhook.event_type,
|
||||
defaultWebhook.enabled,
|
||||
defaultWebhook.headers,
|
||||
defaultWebhook.payload
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Rafraîchir le planificateur de webhooks
|
||||
await webhookScheduler.refreshSchedule();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Webhooks for ${eventType} ${enabled ? 'enabled' : 'disabled'}`,
|
||||
affectedCount: result.rows.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling webhooks:', error);
|
||||
res.status(500).json({ error: 'Failed to toggle webhooks' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -3504,7 +3504,7 @@
|
||||
}
|
||||
},
|
||||
"/stats/getViewsOverTime": {
|
||||
"post": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stats"
|
||||
],
|
||||
@@ -3526,16 +3526,9 @@
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "days",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -3558,7 +3551,7 @@
|
||||
}
|
||||
},
|
||||
"/stats/getViewsByDays": {
|
||||
"post": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stats"
|
||||
],
|
||||
@@ -3580,16 +3573,9 @@
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "days",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -3612,7 +3598,7 @@
|
||||
}
|
||||
},
|
||||
"/stats/getViewsByHour": {
|
||||
"post": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stats"
|
||||
],
|
||||
@@ -3634,16 +3620,56 @@
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
"name": "days",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found"
|
||||
},
|
||||
"503": {
|
||||
"description": "Service Unavailable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stats/getViewsByLibraryType": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stats"
|
||||
],
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "authorization",
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "x-api-token",
|
||||
"in": "header",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "req",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "days",
|
||||
"in": "query",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
const db = require("../db");
|
||||
|
||||
const moment = require("moment");
|
||||
const dayjs = require("dayjs");
|
||||
const { columnsPlayback } = require("../models/jf_playback_activity");
|
||||
const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require("../models/jf_activity_watchdog");
|
||||
const configClass = require("../classes/config");
|
||||
const API = require("../classes/api-loader");
|
||||
const { sendUpdate } = require("../ws");
|
||||
const { isNumber } = require("@mui/x-data-grid/internals");
|
||||
const WebhookManager = require("../classes/webhook-manager");
|
||||
|
||||
const MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK = process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK
|
||||
? Number(process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK)
|
||||
: 1;
|
||||
|
||||
async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
||||
let existingData = await WatchdogData.filter((wdData) => {
|
||||
return SessionData.some((sessionData) => {
|
||||
let NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||
const webhookManager = new WebhookManager();
|
||||
|
||||
let matchesEpisodeId =
|
||||
async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
||||
const existingData = await WatchdogData.filter((wdData) => {
|
||||
return SessionData.some((sessionData) => {
|
||||
const NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||
|
||||
const matchesEpisodeId =
|
||||
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
||||
|
||||
let matchingSessionFound =
|
||||
const matchingSessionFound =
|
||||
// wdData.Id === sessionData.Id &&
|
||||
wdData.UserId === sessionData.UserId &&
|
||||
wdData.DeviceId === sessionData.DeviceId &&
|
||||
@@ -31,16 +35,16 @@ async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
||||
|
||||
//if the playstate was paused, calculate the difference in seconds and add to the playback duration
|
||||
if (sessionData.PlayState.IsPaused == true) {
|
||||
let startTime = moment(wdData.ActivityDateInserted, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
let lastPausedDate = moment(sessionData.LastPausedDate);
|
||||
const startTime = dayjs(wdData.ActivityDateInserted);
|
||||
const lastPausedDate = dayjs(sessionData.LastPausedDate, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
|
||||
let diffInSeconds = lastPausedDate.diff(startTime, "seconds");
|
||||
const diffInSeconds = lastPausedDate.diff(startTime, "seconds");
|
||||
|
||||
wdData.PlaybackDuration = parseInt(wdData.PlaybackDuration) + diffInSeconds;
|
||||
|
||||
wdData.ActivityDateInserted = `${lastPausedDate.format("YYYY-MM-DD HH:mm:ss.SSSZ")}`;
|
||||
} else {
|
||||
wdData.ActivityDateInserted = moment().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
wdData.ActivityDateInserted = dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -52,15 +56,15 @@ async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
||||
}
|
||||
|
||||
async function getSessionsNotInWatchDog(SessionData, WatchdogData) {
|
||||
let newData = await SessionData.filter((sessionData) => {
|
||||
const newData = await SessionData.filter((sessionData) => {
|
||||
if (WatchdogData.length === 0) return true;
|
||||
return !WatchdogData.some((wdData) => {
|
||||
let NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||
const NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||
|
||||
let matchesEpisodeId =
|
||||
const matchesEpisodeId =
|
||||
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
||||
|
||||
let matchingSessionFound =
|
||||
const matchingSessionFound =
|
||||
// wdData.Id === sessionData.Id &&
|
||||
wdData.UserId === sessionData.UserId &&
|
||||
wdData.DeviceId === sessionData.DeviceId &&
|
||||
@@ -75,15 +79,15 @@ async function getSessionsNotInWatchDog(SessionData, WatchdogData) {
|
||||
}
|
||||
|
||||
function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
||||
let removedData = WatchdogData.filter((wdData) => {
|
||||
const removedData = WatchdogData.filter((wdData) => {
|
||||
if (SessionData.length === 0) return true;
|
||||
return !SessionData.some((sessionData) => {
|
||||
let NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||
const NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||
|
||||
let matchesEpisodeId =
|
||||
const matchesEpisodeId =
|
||||
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
||||
|
||||
let noMatchingSessionFound =
|
||||
const noMatchingSessionFound =
|
||||
// wdData.Id === sessionData.Id &&
|
||||
wdData.UserId === sessionData.UserId &&
|
||||
wdData.DeviceId === sessionData.DeviceId &&
|
||||
@@ -97,10 +101,10 @@ function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
||||
|
||||
removedData.map((obj) => {
|
||||
obj.Id = obj.ActivityId;
|
||||
let startTime = moment(obj.ActivityDateInserted, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
let endTime = moment();
|
||||
const startTime = dayjs(obj.ActivityDateInserted);
|
||||
const endTime = dayjs();
|
||||
|
||||
let diffInSeconds = endTime.diff(startTime, "seconds");
|
||||
const diffInSeconds = endTime.diff(startTime, "seconds");
|
||||
|
||||
if (obj.IsPaused == false) {
|
||||
obj.PlaybackDuration = parseInt(obj.PlaybackDuration) + diffInSeconds;
|
||||
@@ -114,20 +118,70 @@ function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
||||
return removedData;
|
||||
}
|
||||
|
||||
async function ActivityMonitor(interval) {
|
||||
// console.log("Activity Interval: " + interval);
|
||||
let currentIntervalId = null;
|
||||
let lastHadActiveSessions = false;
|
||||
let cachedPollingSettings = {
|
||||
activeSessionsInterval: 1000,
|
||||
idleInterval: 5000
|
||||
};
|
||||
|
||||
setInterval(async () => {
|
||||
async function ActivityMonitor(defaultInterval) {
|
||||
// console.log("Activity Monitor started with default interval: " + defaultInterval);
|
||||
|
||||
const runMonitoring = async () => {
|
||||
try {
|
||||
const config = await new configClass().getConfig();
|
||||
|
||||
if (config.error || config.state !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get adaptive polling settings from config
|
||||
const pollingSettings = config.settings?.ActivityMonitorPolling || {
|
||||
activeSessionsInterval: 1000,
|
||||
idleInterval: 5000
|
||||
};
|
||||
|
||||
// Check if polling settings have changed
|
||||
const settingsChanged =
|
||||
cachedPollingSettings.activeSessionsInterval !== pollingSettings.activeSessionsInterval ||
|
||||
cachedPollingSettings.idleInterval !== pollingSettings.idleInterval;
|
||||
|
||||
if (settingsChanged) {
|
||||
console.log('[ActivityMonitor] Polling settings changed, updating intervals');
|
||||
console.log('Old settings:', cachedPollingSettings);
|
||||
console.log('New settings:', pollingSettings);
|
||||
cachedPollingSettings = { ...pollingSettings };
|
||||
}
|
||||
|
||||
const ExcludedUsers = config.settings?.ExcludedUsers || [];
|
||||
const apiSessionData = await API.getSessions();
|
||||
const SessionData = apiSessionData.filter((row) => row.NowPlayingItem !== undefined && !ExcludedUsers.includes(row.UserId));
|
||||
sendUpdate("sessions", apiSessionData);
|
||||
|
||||
const hasActiveSessions = SessionData.length > 0;
|
||||
|
||||
// Determine current appropriate interval
|
||||
const currentInterval = hasActiveSessions ? pollingSettings.activeSessionsInterval : pollingSettings.idleInterval;
|
||||
|
||||
// Check if we need to change the interval (either due to session state change OR settings change)
|
||||
if (hasActiveSessions !== lastHadActiveSessions || settingsChanged) {
|
||||
if (hasActiveSessions !== lastHadActiveSessions) {
|
||||
console.log(`[ActivityMonitor] Switching to ${hasActiveSessions ? 'active' : 'idle'} polling mode (${currentInterval}ms)`);
|
||||
lastHadActiveSessions = hasActiveSessions;
|
||||
}
|
||||
if (settingsChanged) {
|
||||
console.log(`[ActivityMonitor] Applying new ${hasActiveSessions ? 'active' : 'idle'} interval: ${currentInterval}ms`);
|
||||
}
|
||||
|
||||
// Clear current interval and restart with new timing
|
||||
if (currentIntervalId) {
|
||||
clearInterval(currentIntervalId);
|
||||
}
|
||||
currentIntervalId = setInterval(runMonitoring, currentInterval);
|
||||
return; // Let the new interval handle the next execution
|
||||
}
|
||||
|
||||
/////get data from jf_activity_monitor
|
||||
const WatchdogData = await db.query("SELECT * FROM jf_activity_watchdog").then((res) => res.rows);
|
||||
|
||||
@@ -137,15 +191,51 @@ async function ActivityMonitor(interval) {
|
||||
}
|
||||
// New Code
|
||||
|
||||
let WatchdogDataToInsert = await getSessionsNotInWatchDog(SessionData, WatchdogData);
|
||||
let WatchdogDataToUpdate = await getSessionsInWatchDog(SessionData, WatchdogData);
|
||||
let dataToRemove = await getWatchDogNotInSessions(SessionData, WatchdogData);
|
||||
const WatchdogDataToInsert = await getSessionsNotInWatchDog(SessionData, WatchdogData);
|
||||
const WatchdogDataToUpdate = await getSessionsInWatchDog(SessionData, WatchdogData);
|
||||
const dataToRemove = await getWatchDogNotInSessions(SessionData, WatchdogData);
|
||||
|
||||
/////////////////
|
||||
|
||||
//filter fix if table is empty
|
||||
|
||||
if (WatchdogDataToInsert.length > 0) {
|
||||
for (const session of WatchdogDataToInsert) {
|
||||
let userData = {};
|
||||
try {
|
||||
const userInfo = await API.getUserById(session.UserId);
|
||||
if (userInfo) {
|
||||
userData = {
|
||||
username: userInfo.Name,
|
||||
userImageTag: userInfo.PrimaryImageTag
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[WEBHOOK] Error fetching user data: ${error.message}`);
|
||||
}
|
||||
|
||||
await webhookManager.triggerEventWebhooks('playback_started', {
|
||||
sessionInfo: {
|
||||
userId: session.UserId,
|
||||
deviceId: session.DeviceId,
|
||||
deviceName: session.DeviceName,
|
||||
clientName: session.ClientName,
|
||||
isPaused: session.IsPaused,
|
||||
mediaType: session.MediaType,
|
||||
mediaName: session.NowPlayingItemName,
|
||||
startTime: session.ActivityDateInserted
|
||||
},
|
||||
userData,
|
||||
mediaInfo: {
|
||||
itemId: session.NowPlayingItemId,
|
||||
episodeId: session.EpisodeId,
|
||||
mediaName: session.NowPlayingItemName,
|
||||
seasonName: session.SeasonName,
|
||||
seriesName: session.SeriesName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//insert new rows where not existing items
|
||||
// console.log("Inserted " + WatchdogDataToInsert.length + " wd playback records");
|
||||
db.insertBulk("jf_activity_watchdog", WatchdogDataToInsert, jf_activity_watchdog_columns);
|
||||
@@ -158,11 +248,46 @@ async function ActivityMonitor(interval) {
|
||||
console.log("Existing Data Updated: ", WatchdogDataToUpdate.length);
|
||||
}
|
||||
|
||||
if (dataToRemove.length > 0) {
|
||||
for (const session of dataToRemove) {
|
||||
let userData = {};
|
||||
try {
|
||||
const userInfo = await API.getUserById(session.UserId);
|
||||
if (userInfo) {
|
||||
userData = {
|
||||
username: userInfo.Name,
|
||||
userImageTag: userInfo.PrimaryImageTag
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[WEBHOOK] Error fetching user data: ${error.message}`);
|
||||
}
|
||||
|
||||
await webhookManager.triggerEventWebhooks('playback_ended', {
|
||||
sessionInfo: {
|
||||
userId: session.UserId,
|
||||
deviceId: session.DeviceId,
|
||||
deviceName: session.DeviceName,
|
||||
clientName: session.ClientName,
|
||||
playbackDuration: session.PlaybackDuration,
|
||||
endTime: session.ActivityDateInserted
|
||||
},
|
||||
userData,
|
||||
mediaInfo: {
|
||||
itemId: session.NowPlayingItemId,
|
||||
episodeId: session.EpisodeId,
|
||||
mediaName: session.NowPlayingItemName,
|
||||
seasonName: session.SeasonName,
|
||||
seriesName: session.SeriesName
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const toDeleteIds = dataToRemove.map((row) => row.ActivityId);
|
||||
|
||||
//delete from db no longer in session data and insert into stats db
|
||||
//Bulk delete from db thats no longer on api
|
||||
|
||||
const toDeleteIds = dataToRemove.map((row) => row.ActivityId);
|
||||
|
||||
let playbackToInsert = dataToRemove;
|
||||
|
||||
if (playbackToInsert.length == 0 && toDeleteIds.length == 0) {
|
||||
@@ -172,7 +297,7 @@ async function ActivityMonitor(interval) {
|
||||
/////get data from jf_playback_activity within the last hour with progress of <=80% for current items in session
|
||||
|
||||
const ExistingRecords = await db
|
||||
.query(`SELECT * FROM jf_recent_playback_activity(1) limit 0`)
|
||||
.query(`SELECT * FROM jf_recent_playback_activity(1)`)
|
||||
.then((res) => {
|
||||
if (res.rows && Array.isArray(res.rows) && res.rows.length > 0) {
|
||||
return res.rows.filter(
|
||||
@@ -212,7 +337,7 @@ async function ActivityMonitor(interval) {
|
||||
if (existingrow) {
|
||||
playbackData.Id = existingrow.Id;
|
||||
playbackData.PlaybackDuration = Number(existingrow.PlaybackDuration) + Number(playbackData.PlaybackDuration);
|
||||
playbackData.ActivityDateInserted = moment().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
playbackData.ActivityDateInserted = dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -248,7 +373,9 @@ async function ActivityMonitor(interval) {
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error?.code === "ECONNREFUSED") {
|
||||
console.error("Error: Unable to connect to API"); //TO-DO Change this to correct API name
|
||||
} else if (error?.code === "ERR_BAD_RESPONSE") {
|
||||
@@ -258,7 +385,50 @@ async function ActivityMonitor(interval) {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}, interval);
|
||||
};
|
||||
|
||||
// Get initial configuration to start with the correct interval
|
||||
const initConfig = async () => {
|
||||
try {
|
||||
const config = await new configClass().getConfig();
|
||||
|
||||
if (config.error || config.state !== 2) {
|
||||
console.log("[ActivityMonitor] Config not ready, starting with default interval:", defaultInterval + "ms");
|
||||
currentIntervalId = setInterval(runMonitoring, defaultInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get adaptive polling settings from config
|
||||
const pollingSettings = config.settings?.ActivityMonitorPolling || {
|
||||
activeSessionsInterval: 1000,
|
||||
idleInterval: 5000
|
||||
};
|
||||
|
||||
// Initialize cached settings
|
||||
cachedPollingSettings = { ...pollingSettings };
|
||||
|
||||
// Start with idle interval since there are likely no active sessions at startup
|
||||
const initialInterval = pollingSettings.idleInterval;
|
||||
console.log("[ActivityMonitor] Starting adaptive polling with idle interval:", initialInterval + "ms");
|
||||
console.log("[ActivityMonitor] Loaded settings:", pollingSettings);
|
||||
currentIntervalId = setInterval(runMonitoring, initialInterval);
|
||||
|
||||
} catch (error) {
|
||||
console.log("[ActivityMonitor] Error loading config, using default interval:", defaultInterval + "ms");
|
||||
currentIntervalId = setInterval(runMonitoring, defaultInterval);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize with proper configuration
|
||||
await initConfig();
|
||||
|
||||
// Return a cleanup function
|
||||
return () => {
|
||||
if (currentIntervalId) {
|
||||
clearInterval(currentIntervalId);
|
||||
currentIntervalId = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -27,10 +27,10 @@ async function runBackupTask(triggerType = triggertype.Automatic) {
|
||||
|
||||
console.log("Running Scheduled Backup");
|
||||
|
||||
Logging.insertLog(uuid, triggerType, taskName.backup);
|
||||
await Logging.insertLog(uuid, triggerType, taskName.backup);
|
||||
|
||||
await backup(refLog);
|
||||
Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
|
||||
await Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
|
||||
sendUpdate("BackupTask", { type: "Success", message: `${triggerType} Backup Completed` });
|
||||
console.log("Scheduled Backup Complete");
|
||||
parentPort.postMessage({ status: "complete" });
|
||||
@@ -42,8 +42,9 @@ async function runBackupTask(triggerType = triggertype.Automatic) {
|
||||
}
|
||||
}
|
||||
|
||||
parentPort.on("message", (message) => {
|
||||
parentPort.on("message", async (message) => {
|
||||
if (message.command === "start") {
|
||||
runBackupTask(message.triggertype);
|
||||
await runBackupTask(message.triggertype);
|
||||
process.exit(0); // Exit the worker after the task is done
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,8 +28,9 @@ async function runFullSyncTask(triggerType = triggertype.Automatic) {
|
||||
}
|
||||
}
|
||||
|
||||
parentPort.on("message", (message) => {
|
||||
parentPort.on("message", async (message) => {
|
||||
if (message.command === "start") {
|
||||
runFullSyncTask(message.triggertype);
|
||||
await runFullSyncTask(message.triggertype);
|
||||
process.exit(0); // Exit the worker after the task is done
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,8 +27,9 @@ async function runPlaybackReportingPluginSyncTask() {
|
||||
}
|
||||
}
|
||||
|
||||
parentPort.on("message", (message) => {
|
||||
parentPort.on("message", async (message) => {
|
||||
if (message.command === "start") {
|
||||
runPlaybackReportingPluginSyncTask();
|
||||
await runPlaybackReportingPluginSyncTask();
|
||||
process.exit(0); // Exit the worker after the task is done
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { parentPort } = require("worker_threads");
|
||||
const triggertype = require("../logging/triggertype");
|
||||
const sync = require("../routes/sync");
|
||||
const WebhookManager = require("../classes/webhook-manager");
|
||||
|
||||
async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
||||
try {
|
||||
@@ -17,19 +18,33 @@ async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
||||
});
|
||||
parentPort.postMessage({ type: "log", message: formattedArgs.join(" ") });
|
||||
};
|
||||
await sync.partialSync(triggerType);
|
||||
|
||||
const syncResults = await sync.partialSync(triggerType);
|
||||
|
||||
const webhookManager = new WebhookManager();
|
||||
|
||||
const newMediaCount = syncResults?.newItems?.length || 0;
|
||||
|
||||
if (newMediaCount > 0) {
|
||||
await webhookManager.triggerEventWebhooks('media_recently_added', {
|
||||
count: newMediaCount,
|
||||
items: syncResults.newItems,
|
||||
syncDate: new Date().toISOString(),
|
||||
triggerType: triggerType
|
||||
});
|
||||
}
|
||||
|
||||
parentPort.postMessage({ status: "complete" });
|
||||
} catch (error) {
|
||||
parentPort.postMessage({ status: "error", message: error.message });
|
||||
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
parentPort.on("message", (message) => {
|
||||
parentPort.on("message", async (message) => {
|
||||
if (message.command === "start") {
|
||||
runPartialSyncTask(message.triggertype);
|
||||
await runPartialSyncTask(message.triggertype);
|
||||
process.exit(0); // Exit the worker after the task is done
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user