mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Adaptive Polling Implementation
This commit is contained in:
@@ -909,6 +909,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
|
//Jellystat functions
|
||||||
router.get("/CheckForUpdates", async (req, res) => {
|
router.get("/CheckForUpdates", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -114,20 +114,70 @@ function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
|||||||
return removedData;
|
return removedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ActivityMonitor(interval) {
|
let currentIntervalId = null;
|
||||||
// console.log("Activity Interval: " + interval);
|
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 {
|
try {
|
||||||
const config = await new configClass().getConfig();
|
const config = await new configClass().getConfig();
|
||||||
|
|
||||||
if (config.error || config.state !== 2) {
|
if (config.error || config.state !== 2) {
|
||||||
return;
|
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 ExcludedUsers = config.settings?.ExcludedUsers || [];
|
||||||
const apiSessionData = await API.getSessions();
|
const apiSessionData = await API.getSessions();
|
||||||
const SessionData = apiSessionData.filter((row) => row.NowPlayingItem !== undefined && !ExcludedUsers.includes(row.UserId));
|
const SessionData = apiSessionData.filter((row) => row.NowPlayingItem !== undefined && !ExcludedUsers.includes(row.UserId));
|
||||||
sendUpdate("sessions", apiSessionData);
|
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
|
/////get data from jf_activity_monitor
|
||||||
const WatchdogData = await db.query("SELECT * FROM jf_activity_watchdog").then((res) => res.rows);
|
const WatchdogData = await db.query("SELECT * FROM jf_activity_watchdog").then((res) => res.rows);
|
||||||
|
|
||||||
@@ -258,7 +308,50 @@ async function ActivityMonitor(interval) {
|
|||||||
}
|
}
|
||||||
return [];
|
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 = {
|
module.exports = {
|
||||||
|
|||||||
@@ -211,6 +211,15 @@
|
|||||||
"1_DAY": "1 Day",
|
"1_DAY": "1 Day",
|
||||||
"1_WEEK": "1 Week"
|
"1_WEEK": "1 Week"
|
||||||
},
|
},
|
||||||
|
"ACTIVITY_MONITOR": "Activity Monitor",
|
||||||
|
"ACTIVE_SESSIONS_INTERVAL": "Active Sessions Interval (ms)",
|
||||||
|
"ACTIVE_SESSIONS_HELP": "How often to check when users are watching content (recommended: 1000ms)",
|
||||||
|
"IDLE_INTERVAL": "Idle Interval (ms)",
|
||||||
|
"IDLE_HELP": "How often to check when no active sessions (recommended: 5000ms)",
|
||||||
|
"POLLING_INFO_TITLE": "Smart Polling",
|
||||||
|
"POLLING_INFO": "The system automatically adapts monitoring frequency: fast when users are watching content, slower when the server is idle. This reduces CPU load on your Jellyfin server.",
|
||||||
|
"INTERVAL_WARNING": "Active sessions interval should not be greater than idle interval",
|
||||||
|
"REALTIME_UPDATE_INFO": "Changes are applied in real-time without server restart.",
|
||||||
"SELECT_LIBRARIES_TO_IMPORT": "Select Libraries to Import",
|
"SELECT_LIBRARIES_TO_IMPORT": "Select Libraries to Import",
|
||||||
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "Activity for Items within these libraries are still Tracked - Even when not imported.",
|
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "Activity for Items within these libraries are still Tracked - Even when not imported.",
|
||||||
"DATE_ADDED": "Date Added"
|
"DATE_ADDED": "Date Added"
|
||||||
|
|||||||
@@ -211,6 +211,15 @@
|
|||||||
"1_DAY": "1 Jour",
|
"1_DAY": "1 Jour",
|
||||||
"1_WEEK": "1 Semaine"
|
"1_WEEK": "1 Semaine"
|
||||||
},
|
},
|
||||||
|
"ACTIVITY_MONITOR": "Surveillance d'activité",
|
||||||
|
"ACTIVE_SESSIONS_INTERVAL": "Intervalle avec sessions actives (ms)",
|
||||||
|
"ACTIVE_SESSIONS_HELP": "Fréquence de vérification quand des utilisateurs regardent du contenu (recommandé: 1000ms)",
|
||||||
|
"IDLE_INTERVAL": "Intervalle en veille (ms)",
|
||||||
|
"IDLE_HELP": "Fréquence de vérification quand aucune session active (recommandé: 5000ms)",
|
||||||
|
"POLLING_INFO_TITLE": "Polling intelligent",
|
||||||
|
"POLLING_INFO": "Le système adapte automatiquement la fréquence de surveillance : rapide quand des utilisateurs regardent du contenu, plus lent quand le serveur est inactif. Cela réduit la charge CPU sur votre serveur Jellyfin.",
|
||||||
|
"INTERVAL_WARNING": "L'intervalle actif ne devrait pas être supérieur à l'intervalle de veille",
|
||||||
|
"REALTIME_UPDATE_INFO": "Les modifications sont appliquées en temps réel sans redémarrage du serveur.",
|
||||||
"SELECT_LIBRARIES_TO_IMPORT": "Sélectionner les médiathèques à importer",
|
"SELECT_LIBRARIES_TO_IMPORT": "Sélectionner les médiathèques à importer",
|
||||||
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "L'activité du contenu de ces médiathèques est toujours suivie, même s'ils ne sont pas importés.",
|
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "L'activité du contenu de ces médiathèques est toujours suivie, même s'ils ne sont pas importés.",
|
||||||
"DATE_ADDED": "Date d'ajout"
|
"DATE_ADDED": "Date d'ajout"
|
||||||
|
|||||||
175
src/pages/components/settings/ActivityMonitorSettings.jsx
Normal file
175
src/pages/components/settings/ActivityMonitorSettings.jsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import axios from "../../../lib/axios_instance";
|
||||||
|
import { Row, Col, Form, Button, Alert } from "react-bootstrap";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import Loading from "../general/loading";
|
||||||
|
|
||||||
|
export default function ActivityMonitorSettings() {
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
activeSessionsInterval: 1000,
|
||||||
|
idleInterval: 5000
|
||||||
|
});
|
||||||
|
const [isSubmitted, setIsSubmitted] = useState("");
|
||||||
|
const [submissionMessage, setSubmissionMessage] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSettings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSettings = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get("/api/getActivityMonitorSettings", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSettings(response.data);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Activity Monitor settings:", error);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setSettings(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: parseInt(value, 10)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitted("");
|
||||||
|
setSubmissionMessage("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post("/api/setActivityMonitorSettings", settings, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIsSubmitted("Success");
|
||||||
|
setSubmissionMessage("Paramètres de surveillance mis à jour avec succès - Appliqués en temps réel");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating Activity Monitor settings:", error);
|
||||||
|
setIsSubmitted("Failed");
|
||||||
|
setSubmissionMessage(
|
||||||
|
error.response?.data || "Erreur lors de la mise à jour des paramètres"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
<Trans i18nKey={"SETTINGS_PAGE.ACTIVITY_MONITOR"} defaults="Surveillance d'activité" />
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="mb-3 text-muted">
|
||||||
|
<small>
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SETTINGS_PAGE.REALTIME_UPDATE_INFO"}
|
||||||
|
defaults="Les modifications sont appliquées en temps réel sans redémarrage du serveur."
|
||||||
|
/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form onSubmit={handleSubmit} className="settings-form">
|
||||||
|
<Form.Group as={Row} className="mb-3">
|
||||||
|
<Form.Label column className="">
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SETTINGS_PAGE.ACTIVE_SESSIONS_INTERVAL"}
|
||||||
|
defaults="Intervalle avec sessions actives (ms)"
|
||||||
|
/>
|
||||||
|
</Form.Label>
|
||||||
|
<Col sm="10">
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
name="activeSessionsInterval"
|
||||||
|
value={settings.activeSessionsInterval}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
min="500"
|
||||||
|
max="10000"
|
||||||
|
step="100"
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SETTINGS_PAGE.ACTIVE_SESSIONS_HELP"}
|
||||||
|
defaults="Fréquence de vérification quand des utilisateurs regardent du contenu (recommandé: 1000ms)"
|
||||||
|
/>
|
||||||
|
</Form.Text>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group as={Row} className="mb-3">
|
||||||
|
<Form.Label column className="">
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SETTINGS_PAGE.IDLE_INTERVAL"}
|
||||||
|
defaults="Intervalle en veille (ms)"
|
||||||
|
/>
|
||||||
|
</Form.Label>
|
||||||
|
<Col sm="10">
|
||||||
|
<Form.Control
|
||||||
|
type="number"
|
||||||
|
name="idleInterval"
|
||||||
|
value={settings.idleInterval}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
min="1000"
|
||||||
|
max="30000"
|
||||||
|
step="1000"
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SETTINGS_PAGE.IDLE_HELP"}
|
||||||
|
defaults="Fréquence de vérification quand aucune session active (recommandé: 5000ms)"
|
||||||
|
/>
|
||||||
|
</Form.Text>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
{settings.activeSessionsInterval > settings.idleInterval && (
|
||||||
|
<Alert bg="dark" data-bs-theme="dark" variant="warning" className="mb-3">
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SETTINGS_PAGE.INTERVAL_WARNING"}
|
||||||
|
defaults="L'intervalle actif ne devrait pas être supérieur à l'intervalle de veille"
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSubmitted !== "" ? (
|
||||||
|
isSubmitted === "Failed" ? (
|
||||||
|
<Alert bg="dark" data-bs-theme="dark" variant="danger">
|
||||||
|
{submissionMessage}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert bg="dark" data-bs-theme="dark" variant="success">
|
||||||
|
{submissionMessage}
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="d-flex flex-column flex-md-row justify-content-end align-items-md-center">
|
||||||
|
<Button variant="outline-success" type="submit">
|
||||||
|
<Trans i18nKey={"SETTINGS_PAGE.UPDATE"} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import Tasks from "./components/settings/Tasks";
|
|||||||
import SecuritySettings from "./components/settings/security";
|
import SecuritySettings from "./components/settings/security";
|
||||||
import ApiKeys from "./components/settings/apiKeys";
|
import ApiKeys from "./components/settings/apiKeys";
|
||||||
import LibrarySelector from "./library_selector";
|
import LibrarySelector from "./library_selector";
|
||||||
|
import ActivityMonitorSettings from "./components/settings/ActivityMonitorSettings";
|
||||||
|
|
||||||
import Logs from "./components/settings/logs";
|
import Logs from "./components/settings/logs";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export default function Settings() {
|
|||||||
>
|
>
|
||||||
<SettingsConfig />
|
<SettingsConfig />
|
||||||
<SecuritySettings />
|
<SecuritySettings />
|
||||||
|
<ActivityMonitorSettings />
|
||||||
<Tasks />
|
<Tasks />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user