diff --git a/backend/classes/emby-api.js b/backend/classes/emby-api.js
index 57f428d..102b487 100644
--- a/backend/classes/emby-api.js
+++ b/backend/classes/emby-api.js
@@ -11,15 +11,22 @@ class EmbyAPI {
//Helper classes
#checkReadyStatus() {
let checkConfigError = setInterval(async () => {
- const _config = await new configClass().getConfig();
- if (!_config.error && _config.state === 2) {
+ const success = await this.#fetchConfig();
+ if (success) {
clearInterval(checkConfigError);
- this.config = _config;
- this.configReady = true;
}
}, 5000); // Check every 5 seconds
}
+ async #fetchConfig() {
+ const _config = await new configClass().getConfig();
+ if (!_config.error && _config.state === 2) {
+ this.config = _config;
+ this.configReady = true;
+ return true;
+ }
+ return false;
+ }
#errorHandler(error, url) {
if (error.response) {
console.log("[EMBY-API]: " + this.#httpErrorMessageHandler(error));
@@ -292,7 +299,10 @@ class EmbyAPI {
async getLibraries() {
if (!this.configReady) {
- return [];
+ const success = await this.#fetchConfig();
+ if (!success) {
+ return [];
+ }
}
try {
let url = `${this.config.JF_HOST}/Library/MediaFolders`;
diff --git a/backend/classes/jellyfin-api.js b/backend/classes/jellyfin-api.js
index 4546c11..300c420 100644
--- a/backend/classes/jellyfin-api.js
+++ b/backend/classes/jellyfin-api.js
@@ -11,15 +11,23 @@ class JellyfinAPI {
//Helper classes
#checkReadyStatus() {
let checkConfigError = setInterval(async () => {
- const _config = await new configClass().getConfig();
- if (!_config.error && _config.state === 2) {
+ const success = await this.#fetchConfig();
+ if (success) {
clearInterval(checkConfigError);
- this.config = _config;
- this.configReady = true;
}
}, 5000); // Check every 5 seconds
}
+ async #fetchConfig() {
+ const _config = await new configClass().getConfig();
+ if (!_config.error && _config.state === 2) {
+ this.config = _config;
+ this.configReady = true;
+ return true;
+ }
+ return false;
+ }
+
#errorHandler(error, url) {
if (error.response) {
console.log("[JELLYFIN-API]: " + this.#httpErrorMessageHandler(error));
@@ -289,7 +297,10 @@ class JellyfinAPI {
async getLibraries() {
if (!this.configReady) {
- return [];
+ const success = await this.#fetchConfig();
+ if (!success) {
+ return [];
+ }
}
try {
let url = `${this.config.JF_HOST}/Library/MediaFolders`;
diff --git a/backend/classes/task-manager-singleton.js b/backend/classes/task-manager-singleton.js
new file mode 100644
index 0000000..3b4bd2f
--- /dev/null
+++ b/backend/classes/task-manager-singleton.js
@@ -0,0 +1,16 @@
+const TaskManager = require("./task-manager");
+
+class TaskManagerSingleton {
+ constructor() {
+ if (!TaskManagerSingleton.instance) {
+ TaskManagerSingleton.instance = new TaskManager();
+ console.log("Task Manager Singleton created");
+ }
+ }
+
+ getInstance() {
+ return TaskManagerSingleton.instance;
+ }
+}
+
+module.exports = TaskManagerSingleton;
diff --git a/backend/classes/task-manager.js b/backend/classes/task-manager.js
new file mode 100644
index 0000000..7ccf5de
--- /dev/null
+++ b/backend/classes/task-manager.js
@@ -0,0 +1,86 @@
+const { Worker } = require("worker_threads");
+const TaskList = require("../global/task-list");
+const { sendUpdate } = require("../ws");
+
+class TaskManager {
+ constructor() {
+ this.tasks = {};
+ this.taskList = TaskList;
+ this.emitTaskList();
+ }
+
+ addTask({ task, onComplete, onError, onExit }) {
+ if (this.tasks[task.name]) {
+ console.log(`Task ${task.name} already exists.`);
+ return false;
+ }
+
+ const worker = new Worker(task.path);
+
+ worker.on("message", (message) => {
+ if (message.status === "complete" && onComplete) {
+ onComplete();
+ }
+ if (message.status === "error" && onError) {
+ onError(new Error(message.message));
+ }
+ delete this.tasks[task.name];
+ });
+
+ worker.on("error", (error) => {
+ if (onError) {
+ onError(error);
+ }
+ console.error(`Error from ${task.name}:`, error);
+ delete this.tasks[task.name];
+ });
+
+ worker.on("exit", (code) => {
+ if (code !== 0) {
+ console.error(`Worker ${task.name} stopped with exit code ${code}`);
+ }
+ if (onExit) {
+ onExit();
+ }
+ delete this.tasks[task.name];
+ });
+
+ this.tasks[task.name] = { worker };
+ return true;
+ }
+
+ startTask(task, triggerType) {
+ const taskExists = this.tasks[task.name];
+ if (!taskExists) {
+ console.log(`Task ${task.name} does not exist.`);
+ return;
+ }
+ taskExists.worker.postMessage({ command: "start", triggertype: triggerType });
+ }
+
+ stopTask(task) {
+ const taskExists = this.tasks[task.name];
+ if (!taskExists) {
+ console.log(`Task ${task.name} does not exist.`);
+ return;
+ }
+
+ taskExists.worker.terminate();
+ delete this.tasks[task.name];
+ }
+
+ isTaskRunning(taskName) {
+ return !!this.tasks[taskName];
+ }
+
+ emitTaskList() {
+ let emitTasks = setInterval(async () => {
+ const taskList = Object.keys(this.taskList).map((key) => {
+ return { task: key, name: this.taskList[key].name, running: this.isTaskRunning(this.taskList[key].name) };
+ });
+ sendUpdate("task-list", taskList);
+ }, 1000);
+ }
+}
+
+module.exports = TaskManager;
diff --git a/backend/classes/task-scheduler-singleton.js b/backend/classes/task-scheduler-singleton.js
new file mode 100644
index 0000000..2e016eb
--- /dev/null
+++ b/backend/classes/task-scheduler-singleton.js
@@ -0,0 +1,16 @@
+const TaskScheduler = require("./task-scheduler.js");
+
+class TaskSchedulerSingleton {
+ constructor() {
+ if (!TaskSchedulerSingleton.instance) {
+ TaskSchedulerSingleton.instance = new TaskScheduler();
+ console.log("Task Scheduler Singleton created");
+ }
+ }
+
+ getInstance() {
+ return TaskSchedulerSingleton.instance;
+ }
+}
+
+module.exports = TaskSchedulerSingleton;
diff --git a/backend/classes/task-scheduler.js b/backend/classes/task-scheduler.js
new file mode 100644
index 0000000..57ea487
--- /dev/null
+++ b/backend/classes/task-scheduler.js
@@ -0,0 +1,236 @@
+const TaskManager = require("./task-manager-singleton");
+const db = require("../db");
+const TaskList = require("../global/task-list");
+const { sendUpdate } = require("../ws");
+const triggertype = require("../logging/triggertype");
+const taskstate = require("../logging/taskstate");
+
+class TaskScheduler {
+ constructor() {
+ this.taskManager = new TaskManager().getInstance();
+ this.scheduledTasks = {};
+ this.taskHistory = [];
+
+ // Predefined tasks and default intervals (in minutes)
+ this.defaultIntervals = {
+ PartialJellyfinSync: {
+ Interval: 60,
+ ...TaskList.PartialJellyfinSync,
+ },
+ JellyfinSync: {
+ Interval: 1440,
+ ...TaskList.JellyfinSync,
+ },
+ Backup: {
+ Interval: 1440,
+ ...TaskList.Backup,
+ },
+ // Add more tasks as needed
+ };
+
+ // Initialize tasks with default intervals
+ this.initializeTasks();
+ }
+
+ delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ async initializeTasks() {
+ await this.updateIntervalsFromDB();
+ await this.getTaskHistory();
+ await this.clearRunningTasks();
+ this.mainSchedulerUpdateLoop();
+ }
+
+ async clearRunningTasks() {
+ try {
+ await db.query(`UPDATE jf_logging SET "Result"='${taskstate.FAILED}' WHERE "Result"='${taskstate.RUNNING}'`);
+ } catch (error) {
+ console.log("Clear Running Tasks Error: " + error);
+ }
+ }
+
+ async getTaskHistory() {
+ try {
+ const historyjson = await db
+ .query(
+ `
+ with latest_tasks as
+ (SELECT DISTINCT ON ("Name")
+ "Id",
+ "Name",
+ "Type",
+ "ExecutionType",
+ "Duration",
+ "TimeRun",
+ "Log",
+ "Result"
+ FROM public.jf_logging
+ ORDER BY "Name", "TimeRun" DESC
+ )
+
+ select * from latest_tasks
+ ORDER BY "TimeRun" DESC;`
+ )
+ .then((res) =>
+ res.rows.map((row) => {
+ return {
+ Name: row.Name,
+ Type: row.Type,
+ ExecutionType: row.ExecutionType,
+ Duration: row.Duration,
+ TimeRun: row.TimeRun,
+ Result: row.Result,
+ };
+ })
+ );
+
+ this.taskHistory = historyjson;
+ this.getTimeTillNextRun();
+ } catch (error) {
+ console.log("Get Task History Error: " + error);
+ }
+ }
+
+ async updateIntervalsFromDB() {
+ 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 || {};
+
+ for (const taskEnumKey in this.defaultIntervals) {
+ const taskSettings = settings.Tasks?.[taskEnumKey] || {};
+ if (taskSettings.Interval) {
+ this.defaultIntervals[taskEnumKey].Interval = taskSettings.Interval;
+ } else {
+ taskSettings.Interval = this.defaultIntervals[taskEnumKey];
+ }
+
+ if (!settings.Tasks) {
+ settings.Tasks = {};
+ }
+ settings.Tasks[taskEnumKey] = taskSettings;
+ }
+
+ let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
+ await db.query(query, [settings]);
+ }
+ } catch (error) {
+ console.log("Sync Task Settings Error: " + error);
+ }
+ }
+
+ getTimeTillNextRun() {
+ try {
+ for (const taskEnumKey in this.defaultIntervals) {
+ const task = this.defaultIntervals[taskEnumKey];
+ const interval = task.Interval;
+ const lastRun = this.taskHistory.find((history) => history.Name === task.name);
+ const currentTime = new Date().getTime();
+ if (!lastRun) {
+ const nextRunTime = currentTime + interval * 60000;
+ this.defaultIntervals[taskEnumKey].NextRunTime = taskEnumKey == "JellyfinSync" ? 0 : nextRunTime;
+ } else {
+ const lastRunTime = new Date(lastRun.TimeRun).getTime();
+ const nextRunTime = lastRunTime + interval * 60000;
+ this.defaultIntervals[taskEnumKey].NextRunTime = nextRunTime;
+ }
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ }
+
+ mainSchedulerUpdateLoop() {
+ setInterval(() => {
+ const currentTime = new Date().getTime();
+
+ for (const taskEnumKey in this.defaultIntervals) {
+ const task = this.defaultIntervals[taskEnumKey];
+ const nextRunTime = task.NextRunTime;
+ if (currentTime >= nextRunTime && this.taskManager.isTaskRunning(task.name) === false) {
+ console.log(`Running task ${task.name}...`);
+ this.beginTask(taskEnumKey);
+ }
+ }
+ }, 10000);
+ }
+ beginTask(taskEnumKey) {
+ switch (taskEnumKey) {
+ case "PartialJellyfinSync":
+ this.addPartialSyncTask();
+ break;
+ case "JellyfinSync":
+ this.addFullSyncTask();
+ break;
+ case "Backup":
+ this.addBackupTask();
+ break;
+ default:
+ console.log(`Unknown task: ${taskEnumKey}`);
+ }
+ }
+
+ // Add tasks here
+
+ addPartialSyncTask() {
+ const success = this.taskManager.addTask({
+ task: this.taskManager.taskList.PartialJellyfinSync,
+ onComplete: async () => {
+ await this.getTaskHistory();
+ },
+ onError: async (error) => {
+ await this.getTaskHistory();
+ console.error(error);
+ },
+ });
+ if (success) {
+ this.taskManager.startTask(this.taskManager.taskList.PartialJellyfinSync, triggertype.Automatic);
+ return;
+ }
+ }
+
+ addFullSyncTask() {
+ const success = this.taskManager.addTask({
+ task: this.taskManager.taskList.JellyfinSync,
+ onComplete: async () => {
+ await this.getTaskHistory();
+ },
+ onError: async (error) => {
+ await this.getTaskHistory();
+ console.error(error);
+ },
+ });
+ if (success) {
+ this.taskManager.startTask(this.taskManager.taskList.JellyfinSync, triggertype.Automatic);
+ return;
+ }
+ }
+
+ addBackupTask() {
+ const success = this.taskManager.addTask({
+ task: this.taskManager.taskList.Backup,
+ onComplete: async () => {
+ await this.getTaskHistory();
+ sendUpdate("BackupTask", { type: "Success", message: triggertype.Automatic + " Backup Completed" });
+ },
+ onError: async (error) => {
+ console.error(error);
+ await this.getTaskHistory();
+ sendUpdate("BackupTask", { type: "Error", message: "Error: Backup failed" });
+ },
+ onExit: async () => {
+ await this.getTaskHistory();
+ sendUpdate("BackupTask", { type: "Error", message: "Backup Task Stopped" });
+ },
+ });
+ if (success) {
+ this.taskManager.startTask(this.taskManager.taskList.Backup, triggertype.Automatic);
+ return;
+ }
+ }
+}
+
+module.exports = TaskScheduler;
diff --git a/backend/global/task-list.js b/backend/global/task-list.js
new file mode 100644
index 0000000..46fbd66
--- /dev/null
+++ b/backend/global/task-list.js
@@ -0,0 +1,12 @@
+const TaskName = require("../logging/taskName");
+
+const Tasks = {
+ Backup: { path: "./tasks/BackupTask.js", name: TaskName.backup },
+ Restore: { path: "./tasks/BackupTask.js", name: TaskName.restore },
+ JellyfinSync: { path: "./tasks/FullSyncTask.js", name: TaskName.fullsync },
+ PartialJellyfinSync: { path: "./tasks/RecentlyAddedItemsSyncTask.js", name: TaskName.partialsync },
+ JellyfinPlaybackReportingPluginSync: { path: "./tasks/PlaybackReportingPluginSyncTask.js", name: TaskName.import },
+ // Add more tasks as needed
+};
+
+module.exports = Tasks;
diff --git a/backend/routes/api.js b/backend/routes/api.js
index dec2a70..18b96ac 100644
--- a/backend/routes/api.js
+++ b/backend/routes/api.js
@@ -7,13 +7,14 @@ const dbHelper = require("../classes/db-helper");
const pgp = require("pg-promise")();
const { randomUUID } = require("crypto");
-const { axios } = require("../classes/axios");
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 router = express.Router();
@@ -873,6 +874,9 @@ router.post("/setTaskSettings", async (req, res) => {
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
await db.query(query, [settings]);
+ const taskScheduler = new TaskScheduler().getInstance();
+ await taskScheduler.updateIntervalsFromDB();
+ await taskScheduler.getTaskHistory();
res.status(200);
res.send(tasksettings);
} else {
@@ -1863,6 +1867,36 @@ router.post("/getActivityTimeLine", async (req, res) => {
}
});
+//Tasks
+
+router.get("/stopTask", async (req, res) => {
+ const { task } = req.query;
+
+ if (task === undefined) {
+ res.status(400);
+ res.send("No Task provided");
+ return;
+ }
+ const taskManager = new TaskManager().getInstance();
+ if (taskManager.taskList[task] === undefined) {
+ res.status(404);
+ res.send("Task not found");
+ return;
+ }
+
+ const _task = taskManager.taskList[task];
+
+ if (taskManager.isTaskRunning(_task.name)) {
+ taskManager.stopTask(_task);
+ res.send("Task Stopped");
+ return;
+ } else {
+ res.status(400);
+ res.send("Task is not running");
+ return;
+ }
+});
+
// Handle other routes
router.use((req, res) => {
res.status(404).send({ error: "Not Found" });
diff --git a/backend/routes/backup.js b/backend/routes/backup.js
index 807530d..6e768ce 100644
--- a/backend/routes/backup.js
+++ b/backend/routes/backup.js
@@ -6,16 +6,16 @@ const { randomUUID } = require("crypto");
const multer = require("multer");
const Logging = require("../classes/logging");
-const backup = require("../classes/backup");
const triggertype = require("../logging/triggertype");
const taskstate = require("../logging/taskstate");
const taskName = require("../logging/taskName");
const sanitizeFilename = require("../utils/sanitizer");
const { sendUpdate } = require("../ws");
-const db = require("../db");
const router = express.Router();
+const TaskManager = require("../classes/task-manager-singleton");
+const TaskScheduler = require("../classes/task-scheduler-singleton");
// Database connection parameters
const postgresUser = process.env.POSTGRES_USER;
@@ -114,31 +114,28 @@ async function restore(file, refLog) {
// Route handler for backup endpoint
router.get("/beginBackup", async (req, res) => {
try {
- const last_execution = await db
- .query(
- `SELECT "Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.backup}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
-
- if (last_execution.length !== 0) {
- if (last_execution[0].Result === taskstate.RUNNING) {
- sendUpdate("TaskError", "Error: Backup is already running");
- res.send();
- return;
- }
+ const taskManager = new TaskManager().getInstance();
+ const taskScheduler = new TaskScheduler().getInstance();
+ const success = taskManager.addTask({
+ task: taskManager.taskList.Backup,
+ onComplete: async () => {
+ console.log("Backup completed successfully");
+ await taskScheduler.getTaskHistory();
+ res.send("Backup completed successfully");
+ },
+ onError: (error) => {
+ console.error(error);
+ res.status(500).send("Backup failed");
+ sendUpdate("BackupTask", { type: "Error", message: "Error: Backup failed" });
+ },
+ });
+ if (!success) {
+ res.status(500).send("Backup already running");
+ sendUpdate("BackupTask", { type: "Error", message: "Backup is already running" });
+ return;
}
- const uuid = randomUUID();
- let refLog = { logData: [], uuid: uuid };
- await Logging.insertLog(uuid, triggertype.Manual, taskName.backup);
- await backup(refLog);
- Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
- res.send("Backup completed successfully");
- sendUpdate("TaskComplete", { message: triggertype + " Backup Completed" });
+ taskManager.startTask(taskManager.taskList.Backup, triggertype.Manual);
} catch (error) {
console.error(error);
res.status(500).send("Backup failed");
diff --git a/backend/routes/sync.js b/backend/routes/sync.js
index aa11b40..264ce47 100644
--- a/backend/routes/sync.js
+++ b/backend/routes/sync.js
@@ -1,5 +1,4 @@
const express = require("express");
-const pgp = require("pg-promise")();
const db = require("../db");
const moment = require("moment");
@@ -13,6 +12,8 @@ const triggertype = require("../logging/triggertype");
const configClass = require("../classes/config");
const API = require("../classes/api-loader");
+const TaskManager = require("../classes/task-manager-singleton");
+const TaskScheduler = require("../classes/task-scheduler-singleton");
const router = express.Router();
@@ -441,97 +442,113 @@ async function migrateArchivedActivty() {
}
async function syncPlaybackPluginData() {
- PlaybacksyncTask.loggedData.push({ color: "lawngreen", Message: "Syncing..." });
+ try {
+ const uuid = randomUUID();
+ PlaybacksyncTask = { loggedData: [], uuid: uuid };
- //Playback Reporting Plugin Check
- const installed_plugins = await API.getInstalledPlugins();
+ await logging.insertLog(uuid, triggertype.Manual, taskName.import);
+ sendUpdate("PlaybackSyncTask", { type: "Start", message: "Playback Plugin Sync Started" });
- const hasPlaybackReportingPlugin = installed_plugins.filter(
- (plugins) => ["playback_reporting.xml", "Jellyfin.Plugin.PlaybackReporting.xml"].includes(plugins?.ConfigurationFileName) //TO-DO Change this to the correct plugin name
- );
+ PlaybacksyncTask.loggedData.push({ color: "lawngreen", Message: "Syncing..." });
+
+ //Playback Reporting Plugin Check
+ const installed_plugins = await API.getInstalledPlugins();
+
+ const hasPlaybackReportingPlugin = installed_plugins.filter(
+ (plugins) => ["playback_reporting.xml", "Jellyfin.Plugin.PlaybackReporting.xml"].includes(plugins?.ConfigurationFileName) //TO-DO Change this to the correct plugin name
+ );
- if (!hasPlaybackReportingPlugin || hasPlaybackReportingPlugin.length === 0) {
if (!hasPlaybackReportingPlugin || hasPlaybackReportingPlugin.length === 0) {
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: `No new data to insert.` });
- } else {
- PlaybacksyncTask.loggedData.push({ color: "lawngreen", Message: "Playback Reporting Plugin not detected. Skipping step." });
- }
- } else {
- //
-
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Determining query constraints." });
- const OldestPlaybackActivity = await db
- .query('SELECT MIN("ActivityDateInserted") "OldestPlaybackActivity" FROM public.jf_playback_activity')
- .then((res) => res.rows[0]?.OldestPlaybackActivity);
-
- const NewestPlaybackActivity = await db
- .query('SELECT MAX("ActivityDateInserted") "OldestPlaybackActivity" FROM public.jf_playback_activity')
- .then((res) => res.rows[0]?.OldestPlaybackActivity);
-
- const MaxPlaybackReportingPluginID = await db
- .query('SELECT MAX(rowid) "MaxRowId" FROM jf_playback_reporting_plugin_data')
- .then((res) => res.rows[0]?.MaxRowId);
-
- //Query Builder
- 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");
- query = query + ` WHERE (DateCreated < '${formattedDateTimeOld}' or DateCreated > '${formattedDateTimeNew}')`;
- }
-
- if (OldestPlaybackActivity && !NewestPlaybackActivity) {
- const formattedDateTimeOld = moment(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
- query = query + ` WHERE DateCreated < '${formattedDateTimeOld}'`;
- if (MaxPlaybackReportingPluginID) {
- query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
- }
- }
-
- if (!OldestPlaybackActivity && NewestPlaybackActivity) {
- const formattedDateTimeNew = moment(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
- query = query + ` WHERE DateCreated > '${formattedDateTimeNew}'`;
- if (MaxPlaybackReportingPluginID) {
- query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
- }
- }
-
- if (!OldestPlaybackActivity && !NewestPlaybackActivity && MaxPlaybackReportingPluginID) {
- query = query + ` WHERE rowid > ${MaxPlaybackReportingPluginID}`;
- }
-
- query += " order by rowid";
-
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Query built. Executing." });
- //
-
- const PlaybackData = await API.StatsSubmitCustomQuery(query);
-
- let DataToInsert = await PlaybackData.map(mappingPlaybackReporting);
-
- if (DataToInsert.length > 0) {
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: `Inserting ${DataToInsert.length} Rows.` });
- let result = await db.insertBulk("jf_playback_reporting_plugin_data", DataToInsert, columnsPlaybackReporting);
-
- if (result.Result === "SUCCESS") {
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: `${DataToInsert.length} Rows have been inserted.` });
- PlaybacksyncTask.loggedData.push({
- color: "yellow",
- Message: "Running process to format data to be inserted into the Activity Table",
- });
+ if (!hasPlaybackReportingPlugin || hasPlaybackReportingPlugin.length === 0) {
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: `No new data to insert.` });
} else {
- PlaybacksyncTask.loggedData.push({ color: "red", Message: "Error: " + result.message });
- await logging.updateLog(PlaybacksyncTask.uuid, PlaybacksyncTask.loggedData, taskstate.FAILED);
+ PlaybacksyncTask.loggedData.push({
+ color: "lawngreen",
+ Message: "Playback Reporting Plugin not detected. Skipping step.",
+ });
}
+ } else {
+ //
+
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Determining query constraints." });
+ const OldestPlaybackActivity = await db
+ .query('SELECT MIN("ActivityDateInserted") "OldestPlaybackActivity" FROM public.jf_playback_activity')
+ .then((res) => res.rows[0]?.OldestPlaybackActivity);
+
+ const NewestPlaybackActivity = await db
+ .query('SELECT MAX("ActivityDateInserted") "OldestPlaybackActivity" FROM public.jf_playback_activity')
+ .then((res) => res.rows[0]?.OldestPlaybackActivity);
+
+ const MaxPlaybackReportingPluginID = await db
+ .query('SELECT MAX(rowid) "MaxRowId" FROM jf_playback_reporting_plugin_data')
+ .then((res) => res.rows[0]?.MaxRowId);
+
+ //Query Builder
+ 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");
+ query = query + ` WHERE (DateCreated < '${formattedDateTimeOld}' or DateCreated > '${formattedDateTimeNew}')`;
+ }
+
+ if (OldestPlaybackActivity && !NewestPlaybackActivity) {
+ const formattedDateTimeOld = moment(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
+ query = query + ` WHERE DateCreated < '${formattedDateTimeOld}'`;
+ if (MaxPlaybackReportingPluginID) {
+ query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
+ }
+ }
+
+ if (!OldestPlaybackActivity && NewestPlaybackActivity) {
+ const formattedDateTimeNew = moment(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
+ query = query + ` WHERE DateCreated > '${formattedDateTimeNew}'`;
+ if (MaxPlaybackReportingPluginID) {
+ query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
+ }
+ }
+
+ if (!OldestPlaybackActivity && !NewestPlaybackActivity && MaxPlaybackReportingPluginID) {
+ query = query + ` WHERE rowid > ${MaxPlaybackReportingPluginID}`;
+ }
+
+ query += " order by rowid";
+
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Query built. Executing." });
+ //
+
+ const PlaybackData = await API.StatsSubmitCustomQuery(query);
+
+ let DataToInsert = await PlaybackData.map(mappingPlaybackReporting);
+
+ if (DataToInsert.length > 0) {
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: `Inserting ${DataToInsert.length} Rows.` });
+ let result = await db.insertBulk("jf_playback_reporting_plugin_data", DataToInsert, columnsPlaybackReporting);
+
+ if (result.Result === "SUCCESS") {
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: `${DataToInsert.length} Rows have been inserted.` });
+ PlaybacksyncTask.loggedData.push({
+ color: "yellow",
+ Message: "Running process to format data to be inserted into the Activity Table",
+ });
+ } else {
+ PlaybacksyncTask.loggedData.push({ color: "red", Message: "Error: " + result.message });
+ await logging.updateLog(PlaybacksyncTask.uuid, PlaybacksyncTask.loggedData, taskstate.FAILED);
+ }
+ }
+
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Process complete. Data has been imported." });
}
+ await db.query("CALL ji_insert_playback_plugin_data_to_activity_table()");
+ PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Any imported data has been processed." });
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Process complete. Data has been imported." });
+ PlaybacksyncTask.loggedData.push({ color: "lawngreen", Message: `Playback Reporting Plugin Sync Complete` });
+ await logging.updateLog(PlaybacksyncTask.uuid, PlaybacksyncTask.loggedData, taskstate.SUCCESS);
+ } catch (error) {
+ PlaybacksyncTask.loggedData.push({ color: "red", Message: `Error: ${error}` });
+ await logging.updateLog(PlaybacksyncTask.uuid, PlaybacksyncTask.loggedData, taskstate.FAILED);
+ sendUpdate("PlaybackSyncTask", { type: "Error", message: "Error: Playback Plugin Sync failed" });
}
- await db.query("CALL ji_insert_playback_plugin_data_to_activity_table()");
- PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Any imported data has been processed." });
-
- PlaybacksyncTask.loggedData.push({ color: "lawngreen", Message: `Playback Reporting Plugin Sync Complete` });
}
async function updateLibraryStatsData() {
@@ -970,63 +987,73 @@ async function partialSync(triggertype) {
///////////////////////////////////////Sync All
router.get("/beginSync", async (req, res) => {
- const config = await new configClass().getConfig();
+ try {
+ const taskManager = new TaskManager().getInstance();
+ const taskScheduler = new TaskScheduler().getInstance();
+ const success = taskManager.addTask({
+ task: taskManager.taskList.JellyfinSync,
+ onComplete: async () => {
+ console.log("Full Sync completed successfully");
+ await taskScheduler.getTaskHistory();
+ res.send("Full Sync completed successfully");
- if (config.error) {
- res.send({ error: "Config Details Not Found" });
- return;
- }
-
- const last_execution = await db
- .query(
- `SELECT "Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.fullsync}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
-
- if (last_execution.length !== 0) {
- if (last_execution[0].Result === taskstate.RUNNING) {
- sendUpdate("TaskError", "Error: Sync is already running");
- res.send();
+ sendUpdate("FullSyncTask", { type: "Success", message: triggertype.Manual + " Full Sync Completed" });
+ },
+ onError: async (error) => {
+ console.error(error);
+ await taskScheduler.getTaskHistory();
+ res.status(500).send("Full Sync failed");
+ sendUpdate("FullSyncTask", { type: "Error", message: "Error: Full Sync failed" });
+ },
+ });
+ if (!success) {
+ res.status(500).send("Full Sync already running");
+ sendUpdate("FullSyncTask", { type: "Error", message: "Full Sync is already running" });
return;
}
- }
- await fullSync(triggertype.Manual);
- res.send();
+ taskManager.startTask(taskManager.taskList.JellyfinSync, triggertype.Manual);
+ } catch (error) {
+ console.error(error);
+ res.status(500).send("Full Sync failed");
+ }
});
router.get("/beginPartialSync", async (req, res) => {
- const config = await new configClass().getConfig();
+ try {
+ const taskManager = new TaskManager().getInstance();
+ const taskScheduler = new TaskScheduler().getInstance();
+ const success = taskManager.addTask({
+ task: taskManager.taskList.PartialJellyfinSync,
+ onComplete: async () => {
+ console.log("Recently Added Items Sync completed successfully");
+ await taskScheduler.getTaskHistory();
+ res.send("Recently Added Items Sync completed successfully");
- if (config.error) {
- res.send({ error: config.error });
- return;
- }
-
- const last_execution = await db
- .query(
- `SELECT "Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.partialsync}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
-
- if (last_execution.length !== 0) {
- if (last_execution[0].Result === taskstate.RUNNING) {
- sendUpdate("TaskError", "Error: Sync is already running");
- res.send();
+ sendUpdate("PartialSyncTask", { type: "Success", message: triggertype.Manual + " Recently Added Items Sync Completed" });
+ },
+ onError: async (error) => {
+ await taskScheduler.getTaskHistory();
+ console.error(error);
+ res.status(500).send("Recently Added Items Sync failed");
+ sendUpdate("PartialSyncTask", { type: "Error", message: "Error: Recently Added Items Sync failed" });
+ },
+ onExit: async () => {
+ await taskScheduler.getTaskHistory();
+ sendUpdate("PartialSyncTask", { type: "Error", message: "Task Stopped" });
+ },
+ });
+ if (!success) {
+ res.status(500).send("Recently Added Items Sync already running");
+ sendUpdate("PartialSyncTask", { type: "Error", message: "Recently Added Items Sync is already running" });
return;
}
- }
- await partialSync(triggertype.Manual);
- res.send();
+ taskManager.startTask(taskManager.taskList.PartialJellyfinSync, triggertype.Manual);
+ } catch (error) {
+ console.error(error);
+ res.status(500).send("Recently Added Items Sync failed");
+ }
});
///////////////////////////////////////Write Users
@@ -1143,31 +1170,37 @@ router.post("/fetchItem", async (req, res) => {
//////////////////////////////////////////////////////syncPlaybackPluginData
router.get("/syncPlaybackPluginData", async (req, res) => {
- const config = await new configClass().getConfig();
-
- const uuid = randomUUID();
- PlaybacksyncTask = { loggedData: [], uuid: uuid };
try {
- await logging.insertLog(uuid, triggertype.Manual, taskName.import);
- sendUpdate("PlaybackSyncTask", { type: "Start", message: "Playback Plugin Sync Started" });
+ const taskManager = new TaskManager().getInstance();
+ const taskScheduler = new TaskScheduler().getInstance();
+ const success = taskManager.addTask({
+ task: taskManager.taskList.JellyfinPlaybackReportingPluginSync,
+ onComplete: async () => {
+ console.log("Playback Plugin Sync completed successfully");
- if (config.error) {
- res.send({ error: config.error });
- PlaybacksyncTask.loggedData.push({ Message: config.error });
- await logging.updateLog(uuid, PlaybacksyncTask.loggedData, taskstate.FAILED);
+ await taskScheduler.getTaskHistory();
+ res.send("Playback Plugin Sync completed successfully");
+ },
+ onError: async (error) => {
+ await taskScheduler.getTaskHistory();
+ console.error(error);
+ res.status(500).send("Playback Plugin Sync failed");
+ },
+ onExit: async () => {
+ await taskScheduler.getTaskHistory();
+ sendUpdate("PlaybackSyncTask", { type: "Error", message: "Task Stopped" });
+ },
+ });
+ if (!success) {
+ res.status(500).send("Playback Plugin Sync already running");
+ sendUpdate("PlaybackSyncTask", { type: "Error", message: "Playback Plugin Sync is already running" });
return;
}
- await sleep(5000);
- await syncPlaybackPluginData();
-
- await logging.updateLog(PlaybacksyncTask.uuid, PlaybacksyncTask.loggedData, taskstate.SUCCESS);
- sendUpdate("PlaybackSyncTask", { type: "Success", message: "Playback Plugin Sync Completed" });
- res.send("syncPlaybackPluginData Complete");
+ taskManager.startTask(taskManager.taskList.JellyfinPlaybackReportingPluginSync, triggertype.Manual, PlaybacksyncTask);
} catch (error) {
- PlaybacksyncTask.loggedData.push({ color: "red", Message: getErrorLineNumber(error) + ": Error: " + error });
- await logging.updateLog(PlaybacksyncTask.uuid, PlaybacksyncTask.loggedData, taskstate.FAILED);
- res.send("syncPlaybackPluginData Halted with Errors");
+ console.error(error);
+ res.status(500).send("Playback Plugin Sync failed");
}
});
@@ -1188,4 +1221,5 @@ module.exports = {
router,
fullSync,
partialSync,
+ syncPlaybackPluginData,
};
diff --git a/backend/server.js b/backend/server.js
index c1004a5..ed59fd6 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -28,7 +28,9 @@ const utilsRouter = require("./routes/utils");
// tasks
const ActivityMonitor = require("./tasks/ActivityMonitor");
-const tasks = require("./tasks/tasks");
+const TaskManager = require("./classes/task-manager-singleton");
+const TaskScheduler = require("./classes/task-scheduler-singleton");
+// const tasks = require("./tasks/tasks");
// websocket
const { setupWebSocketServer } = require("./ws");
@@ -239,9 +241,8 @@ try {
server.listen(PORT, LISTEN_IP, async () => {
console.log(`[JELLYSTAT] Server listening on http://127.0.0.1:${PORT}`);
ActivityMonitor.ActivityMonitor(1000);
- tasks.FullSyncTask();
- tasks.RecentlyAddedItemsSyncTask();
- tasks.BackupTask();
+ new TaskManager();
+ new TaskScheduler();
});
});
});
diff --git a/backend/socket-io-client.js b/backend/socket-io-client.js
new file mode 100644
index 0000000..7a7e380
--- /dev/null
+++ b/backend/socket-io-client.js
@@ -0,0 +1,30 @@
+const io = require("socket.io-client");
+
+class SocketIoClient {
+ constructor(serverUrl) {
+ this.serverUrl = serverUrl;
+ this.client = null;
+ }
+
+ connect() {
+ this.client = io(this.serverUrl);
+ }
+
+ waitForConnection() {
+ return new Promise((resolve) => {
+ if (this.client && this.client.connected) {
+ resolve();
+ } else {
+ this.client.on("connect", resolve);
+ }
+ });
+ }
+
+ sendMessage(message) {
+ if (this.client && this.client.connected) {
+ this.client.emit("message", JSON.stringify(message));
+ }
+ }
+}
+
+module.exports = SocketIoClient;
diff --git a/backend/tasks/BackupTask.js b/backend/tasks/BackupTask.js
index fa9e7c2..c9dd25a 100644
--- a/backend/tasks/BackupTask.js
+++ b/backend/tasks/BackupTask.js
@@ -1,128 +1,36 @@
-const db = require("../db");
+const { parentPort } = require("worker_threads");
const Logging = require("../classes/logging");
-const configClass =require("../classes/config");
-
const backup = require("../classes/backup");
-const moment = require('moment');
-const { randomUUID } = require('crypto');
+const { randomUUID } = require("crypto");
const taskstate = require("../logging/taskstate");
const taskName = require("../logging/taskName");
const triggertype = require("../logging/triggertype");
+const { sendUpdate } = require("../ws");
+async function runBackupTask(triggerType = triggertype.Automatic) {
+ try {
+ const uuid = randomUUID();
+ const refLog = { logData: [], uuid: uuid };
-async function BackupTask() {
- try{
+ console.log("Running Scheduled Backup");
- await db.query(
- `UPDATE jf_logging SET "Result"='${taskstate.FAILED}' WHERE "Name"='${taskName.backup}' AND "Result"='${taskstate.RUNNING}'`
- );
- }
- catch(error)
- {
- console.log('Error Cleaning up Backup Tasks: '+error);
- }
-let interval=10000;
-let taskDelay=1440; // 1 day in minutes
+ Logging.insertLog(uuid, triggerType, taskName.backup);
+ await backup(refLog);
+ Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
+ sendUpdate("BackupTask", { type: "Success", message: `${triggerType} Backup Completed` });
+ console.log("Scheduled Backup Complete");
+ parentPort.postMessage({ status: "complete" });
+ } catch (error) {
+ parentPort.postMessage({ status: "error", message: error.message });
-try{//get interval from db
-
-
- const settingsjson = await db
- .query('SELECT settings FROM app_config where "ID"=1')
- .then((res) => res.rows);
-
- if (settingsjson.length > 0) {
- const settings = settingsjson[0].settings || {};
-
- let backuptasksettings = settings.Tasks?.Backup || {};
-
- if (backuptasksettings.Interval) {
- taskDelay=backuptasksettings.Interval;
- } else {
- backuptasksettings.Interval=taskDelay;
- }
-
- if(!settings.Tasks)
- {
- settings.Tasks = {};
- }
- if(!settings.Tasks.Backup)
- {
- settings.Tasks.Backup = {};
- }
- settings.Tasks.Backup = backuptasksettings;
-
-
- let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
-
- await db.query(query, [settings]);
+ console.log(error);
+ return [];
}
}
-catch(error)
-{
- console.log('Sync Task Settings Error: '+error);
-}
-async function intervalCallback() {
- clearInterval(intervalTask);
- try{
- let current_time = moment();
- const config = await new configClass().getConfig();
-
- if (config.error)
- {
- return;
- }
-
-
- const last_execution=await db.query( `SELECT "TimeRun","Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.backup}' AND "Result" in ('${taskstate.SUCCESS}','${taskstate.RUNNING}')
- ORDER BY "TimeRun" DESC
- LIMIT 1`).then((res) => res.rows);
-
- if(last_execution.length!==0)
- {
- let last_execution_time = moment(last_execution[0].TimeRun).add(taskDelay, 'minutes');
-
- if(!current_time.isAfter(last_execution_time) || last_execution[0].Result ===taskstate.RUNNING)
- {
-
- intervalTask = setInterval(intervalCallback, interval);
- return;
- }
- }
-
- const uuid = randomUUID();
- let refLog={logData:[],uuid:uuid};
-
-
- console.log('Running Scheduled Backup');
-
- Logging.insertLog(uuid,triggertype.Automatic,taskName.backup);
-
-
- await backup(refLog);
- Logging.updateLog(uuid,refLog.logData,taskstate.SUCCESS);
-
-
- console.log('Scheduled Backup Complete');
-
- } catch (error)
- {
- console.log(error);
- return [];
- }
-
- intervalTask = setInterval(intervalCallback, interval);
- }
-
- let intervalTask = setInterval(intervalCallback, interval);
-
-
-}
-
-module.exports = {
- BackupTask,
-};
+parentPort.on("message", (message) => {
+ if (message.command === "start") {
+ runBackupTask(message.triggertype);
+ }
+});
diff --git a/backend/tasks/FullSyncTask.js b/backend/tasks/FullSyncTask.js
index 337bba5..47f9a4a 100644
--- a/backend/tasks/FullSyncTask.js
+++ b/backend/tasks/FullSyncTask.js
@@ -1,115 +1,22 @@
-const db = require("../db");
-const moment = require("moment");
-const sync = require("../routes/sync");
-const taskName = require("../logging/taskName");
-const taskstate = require("../logging/taskstate");
+const { parentPort } = require("worker_threads");
const triggertype = require("../logging/triggertype");
+const sync = require("../routes/sync");
-async function FullSyncTask() {
+async function runFullSyncTask(triggerType = triggertype.Automatic) {
try {
- await db.query(
- `UPDATE jf_logging SET "Result"='${taskstate.FAILED}' WHERE "Name"='${taskName.fullsync}' AND "Result"='${taskstate.RUNNING}'`
- );
+ await sync.fullSync(triggerType);
+
+ parentPort.postMessage({ status: "complete" });
} catch (error) {
- console.log("Error Cleaning up Sync Tasks: " + error);
+ parentPort.postMessage({ status: "error", message: error.message });
+
+ console.log(error);
+ return [];
}
-
- let interval = 10000;
-
- let taskDelay = 1440; //in minutes
-
- async function fetchTaskSettings() {
- try {
- //get interval from db
-
- const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
-
- if (settingsjson.length > 0) {
- const settings = settingsjson[0].settings || {};
-
- let synctasksettings = settings.Tasks?.JellyfinSync || {};
-
- if (synctasksettings.Interval) {
- taskDelay = synctasksettings.Interval;
- } else {
- synctasksettings.Interval = taskDelay;
-
- if (!settings.Tasks) {
- settings.Tasks = {};
- }
- if (!settings.Tasks.JellyfinSync) {
- settings.Tasks.JellyfinSync = {};
- }
- settings.Tasks.JellyfinSync = synctasksettings;
-
- let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
-
- await db.query(query, [settings]);
- }
- }
- } catch (error) {
- console.log("Sync Task Settings Error: " + error);
- }
- }
-
- async function intervalCallback() {
- clearInterval(intervalTask);
- try {
- let current_time = moment();
- const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
-
- if (config.length === 0 || config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
- return;
- }
-
- const last_execution = await db
- .query(
- `SELECT "TimeRun","Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.fullsync}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
-
- const last_execution_partialSync = await db
- .query(
- `SELECT "TimeRun","Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.partialsync}'
- AND "Result"='${taskstate.RUNNING}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
- if (last_execution.length !== 0) {
- await fetchTaskSettings();
- let last_execution_time = moment(last_execution[0].TimeRun).add(taskDelay, "minutes");
-
- if (
- !current_time.isAfter(last_execution_time) ||
- last_execution[0].Result === taskstate.RUNNING ||
- last_execution_partialSync.length > 0
- ) {
- intervalTask = setInterval(intervalCallback, interval);
- return;
- }
- }
-
- console.log("Running Scheduled Sync");
- await sync.fullSync(triggertype.Automatic);
- console.log("Scheduled Sync Complete");
- } catch (error) {
- console.log(error);
- return [];
- }
-
- intervalTask = setInterval(intervalCallback, interval);
- }
-
- let intervalTask = setInterval(intervalCallback, interval);
}
-module.exports = {
- FullSyncTask,
-};
+parentPort.on("message", (message) => {
+ if (message.command === "start") {
+ runFullSyncTask(message.triggertype);
+ }
+});
diff --git a/backend/tasks/PlaybackReportingPluginSyncTask.js b/backend/tasks/PlaybackReportingPluginSyncTask.js
new file mode 100644
index 0000000..467b855
--- /dev/null
+++ b/backend/tasks/PlaybackReportingPluginSyncTask.js
@@ -0,0 +1,21 @@
+const { parentPort } = require("worker_threads");
+const sync = require("../routes/sync");
+
+async function runPlaybackReportingPluginSyncTask() {
+ try {
+ await sync.syncPlaybackPluginData();
+
+ parentPort.postMessage({ status: "complete" });
+ } catch (error) {
+ parentPort.postMessage({ status: "error", message: error.message });
+
+ console.log(error);
+ return [];
+ }
+}
+
+parentPort.on("message", (message) => {
+ if (message.command === "start") {
+ runPlaybackReportingPluginSyncTask();
+ }
+});
diff --git a/backend/tasks/RecentlyAddedItemsSyncTask.js b/backend/tasks/RecentlyAddedItemsSyncTask.js
index 4c83eee..969720f 100644
--- a/backend/tasks/RecentlyAddedItemsSyncTask.js
+++ b/backend/tasks/RecentlyAddedItemsSyncTask.js
@@ -1,115 +1,22 @@
-const db = require("../db");
-const moment = require("moment");
-const sync = require("../routes/sync");
-const taskName = require("../logging/taskName");
-const taskstate = require("../logging/taskstate");
+const { parentPort } = require("worker_threads");
const triggertype = require("../logging/triggertype");
+const sync = require("../routes/sync");
-async function RecentlyAddedItemsSyncTask() {
+async function runPartialSyncTask(triggerType = triggertype.Automatic) {
try {
- await db.query(
- `UPDATE jf_logging SET "Result"='${taskstate.FAILED}' WHERE "Name"='${taskName.partialsync}' AND "Result"='${taskstate.RUNNING}'`
- );
+ await sync.partialSync(triggerType);
+
+ parentPort.postMessage({ status: "complete" });
} catch (error) {
- console.log("Error Cleaning up Sync Tasks: " + error);
+ parentPort.postMessage({ status: "error", message: error.message });
+
+ console.log(error);
+ return [];
}
-
- let interval = 11000;
-
- let taskDelay = 60; //in minutes
-
- async function fetchTaskSettings() {
- try {
- //get interval from db
-
- const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
-
- if (settingsjson.length > 0) {
- const settings = settingsjson[0].settings || {};
-
- let synctasksettings = settings.Tasks?.PartialJellyfinSync || {};
-
- if (synctasksettings.Interval) {
- taskDelay = synctasksettings.Interval;
- } else {
- synctasksettings.Interval = taskDelay;
-
- if (!settings.Tasks) {
- settings.Tasks = {};
- }
- if (!settings.Tasks.PartialJellyfinSync) {
- settings.Tasks.PartialJellyfinSync = {};
- }
- settings.Tasks.PartialJellyfinSync = synctasksettings;
-
- let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
-
- await db.query(query, [settings]);
- }
- }
- } catch (error) {
- console.log("Sync Task Settings Error: " + error);
- }
- }
-
- async function intervalCallback() {
- clearInterval(intervalTask);
- try {
- let current_time = moment();
- const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
-
- if (!config || config.length === 0 || config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
- return;
- }
-
- const last_execution = await db
- .query(
- `SELECT "TimeRun","Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.partialsync}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
-
- const last_execution_FullSync = await db
- .query(
- `SELECT "TimeRun","Result"
- FROM public.jf_logging
- WHERE "Name"='${taskName.fullsync}'
- AND "Result"='${taskstate.RUNNING}'
- ORDER BY "TimeRun" DESC
- LIMIT 1`
- )
- .then((res) => res.rows);
- if (last_execution.length !== 0) {
- await fetchTaskSettings();
- let last_execution_time = moment(last_execution[0].TimeRun).add(taskDelay, "minutes");
-
- if (
- !current_time.isAfter(last_execution_time) ||
- last_execution[0].Result === taskstate.RUNNING ||
- last_execution_FullSync.length > 0
- ) {
- intervalTask = setInterval(intervalCallback, interval);
- return;
- }
- }
-
- console.log("Running Recently Added Scheduled Sync");
- await sync.partialSync(triggertype.Automatic);
- console.log("Scheduled Recently Added Sync Complete");
- } catch (error) {
- console.log(error);
- return [];
- }
-
- intervalTask = setInterval(intervalCallback, interval);
- }
-
- let intervalTask = setInterval(intervalCallback, interval);
}
-module.exports = {
- RecentlyAddedItemsSyncTask,
-};
+parentPort.on("message", (message) => {
+ if (message.command === "start") {
+ runPartialSyncTask(message.triggertype);
+ }
+});
diff --git a/backend/tasks/tasks.js b/backend/tasks/tasks.js
deleted file mode 100644
index 5ebbfe2..0000000
--- a/backend/tasks/tasks.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const { BackupTask } = require("./BackupTask");
-const { RecentlyAddedItemsSyncTask } = require("./RecentlyAddedItemsSyncTask");
-const { FullSyncTask } = require("./FullSyncTask");
-
-const tasks = {
- FullSyncTask:FullSyncTask,
- RecentlyAddedItemsSyncTask:RecentlyAddedItemsSyncTask,
- BackupTask:BackupTask,
- };
-module.exports = tasks;
\ No newline at end of file
diff --git a/backend/ws-server-singleton.js b/backend/ws-server-singleton.js
new file mode 100644
index 0000000..82a6fd8
--- /dev/null
+++ b/backend/ws-server-singleton.js
@@ -0,0 +1,17 @@
+class WebSocketServerSingleton {
+ constructor() {
+ if (!WebSocketServerSingleton.instance) {
+ WebSocketServerSingleton.instance = null;
+ }
+ }
+
+ setInstance(io) {
+ WebSocketServerSingleton.instance = io;
+ }
+
+ getInstance() {
+ return WebSocketServerSingleton.instance;
+ }
+}
+
+module.exports = new WebSocketServerSingleton();
diff --git a/backend/ws.js b/backend/ws.js
index 58afb10..031feeb 100644
--- a/backend/ws.js
+++ b/backend/ws.js
@@ -1,29 +1,46 @@
// ws.js
const socketIO = require("socket.io");
+const webSocketServerSingleton = require("./ws-server-singleton.js");
+const SocketIoClient = require("./socket-io-client.js");
+const socketClient = new SocketIoClient("http://127.0.0.1:3000");
let io; // Store the socket.io server instance
const setupWebSocketServer = (server, namespacePath) => {
- io = socketIO(server, { path: namespacePath + "/socket.io" }); // Create the socket.io server
+ io = socketIO(server, { path: namespacePath + "/socket.io" });
+
+ socketClient.connect();
io.on("connection", (socket) => {
// console.log("Client connected to namespace:", namespacePath);
socket.on("message", (message) => {
- console.log(`Received: ${message}`);
+ const payload = JSON.parse(message);
+ sendUpdate(payload.tag, payload.message);
});
});
+
+ webSocketServerSingleton.setInstance(io);
};
const sendToAllClients = (message) => {
- if (io) {
- io.emit("message", message);
+ const ioInstance = webSocketServerSingleton.getInstance();
+ if (ioInstance) {
+ ioInstance.emit("message", message);
}
};
-const sendUpdate = (tag, message) => {
- if (io) {
- io.emit(tag, message);
+const sendUpdate = async (tag, message) => {
+ const ioInstance = webSocketServerSingleton.getInstance();
+ if (ioInstance) {
+ ioInstance.emit(tag, message);
+ } else {
+ if (socketClient.client == null || socketClient.client.connected == false) {
+ socketClient.connect();
+ await socketClient.waitForConnection();
+ }
+
+ socketClient.sendMessage({ tag: tag, message: message });
}
};
diff --git a/package-lock.json b/package-lock.json
index 6b0a0a1..2559d5c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "jfstat",
- "version": "1.1.3",
+ "version": "1.1.4",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
@@ -70,7 +70,7 @@
"swagger-autogen": "^2.23.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
- "ws": "^8.13.0"
+ "ws": "^8.18.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
@@ -9326,6 +9326,26 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
+ "node_modules/engine.io-client/node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/engine.io-parser": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
@@ -22647,9 +22667,9 @@
}
},
"node_modules/ws": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
- "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"engines": {
"node": ">=10.0.0"
},
diff --git a/package.json b/package.json
index 3b9d3f0..e201aaa 100644
--- a/package.json
+++ b/package.json
@@ -77,7 +77,7 @@
"swagger-autogen": "^2.23.5",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
- "ws": "^8.13.0"
+ "ws": "^8.18.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
diff --git a/src/lib/tasklist.jsx b/src/lib/tasklist.jsx
index 2c0e43b..f8a5736 100644
--- a/src/lib/tasklist.jsx
+++ b/src/lib/tasklist.jsx
@@ -17,7 +17,7 @@ export const taskList = [
},
{
id: 2,
- name: "Jellyfin Playback Reporting Plugin Sync",
+ name: "JellyfinPlaybackReportingPluginSync",
description: ,
type: "IMPORT",
link: "/sync/syncPlaybackPluginData",
diff --git a/src/pages/components/settings/Task.jsx b/src/pages/components/settings/Task.jsx
index 452bcff..4972b66 100644
--- a/src/pages/components/settings/Task.jsx
+++ b/src/pages/components/settings/Task.jsx
@@ -8,57 +8,65 @@ import i18next from "i18next";
import { Trans } from "react-i18next";
import "../../css/settings/settings.css";
-function Task({ task, processing, taskIntervals, updateTask, onClick }) {
- const intervals = [
- { value: 15, display: i18next.t("SETTINGS_PAGE.INTERVALS.15_MIN") },
- { value: 30, display: i18next.t("SETTINGS_PAGE.INTERVALS.30_MIN") },
- { value: 60, display: i18next.t("SETTINGS_PAGE.INTERVALS.1_HOUR") },
- { value: 720, display: i18next.t("SETTINGS_PAGE.INTERVALS.12_HOURS") },
- { value: 1440, display: i18next.t("SETTINGS_PAGE.INTERVALS.1_DAY") },
- { value: 10080, display: i18next.t("SETTINGS_PAGE.INTERVALS.1_WEEK") },
- ];
- return (
-
- {task.description}
-
-
-
-
-
- {task.type === "JOB" ? (
-
-
- {taskIntervals &&
- intervals.find((interval) => interval.value === (taskIntervals[task.name]?.Interval || 15)).display}
-
-
- {taskIntervals &&
- intervals.map((interval) => (
- updateTask(task.name, interval.value)}
- value={interval.value}
- key={interval.value}
- >
- {interval.display}
-
- ))}
-
-
- ) : (
- <>>
- )}
-
-
-
-
-
- );
- }
+function Task({ task, taskState, processing, taskIntervals, updateTask, onClick, stopTask }) {
+ const intervals = [
+ { value: 15, display: i18next.t("SETTINGS_PAGE.INTERVALS.15_MIN") },
+ { value: 30, display: i18next.t("SETTINGS_PAGE.INTERVALS.30_MIN") },
+ { value: 60, display: i18next.t("SETTINGS_PAGE.INTERVALS.1_HOUR") },
+ { value: 720, display: i18next.t("SETTINGS_PAGE.INTERVALS.12_HOURS") },
+ { value: 1440, display: i18next.t("SETTINGS_PAGE.INTERVALS.1_DAY") },
+ { value: 10080, display: i18next.t("SETTINGS_PAGE.INTERVALS.1_WEEK") },
+ ];
+ const state = taskState ? taskState.filter((state) => state.task === task.name)[0] : null;
- export default Task
\ No newline at end of file
+ return (
+
+ {task.description}
+
+
+
+
+
+ {task.type === "JOB" ? (
+
+
+ {taskIntervals &&
+ intervals.find((interval) => interval.value === (taskIntervals[task.name]?.Interval || 15)).display}
+
+
+ {taskIntervals &&
+ intervals.map((interval) => (
+ updateTask(task.name, interval.value)}
+ value={interval.value}
+ key={interval.value}
+ >
+ {interval.display}
+
+ ))}
+
+
+ ) : (
+ <>>
+ )}
+
+
+ {state ? (
+ state.running == true ? (
+
+ ) : (
+
+ )
+ ) : (
+ <>>
+ )}
+
+
+ );
+}
+
+export default Task;
diff --git a/src/pages/components/settings/Tasks.jsx b/src/pages/components/settings/Tasks.jsx
index d1559ad..30b379a 100644
--- a/src/pages/components/settings/Tasks.jsx
+++ b/src/pages/components/settings/Tasks.jsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import axios from "../../../lib/axios_instance";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
@@ -6,8 +6,9 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
-import { taskList } from "../../../lib/tasklist";
+import { taskList } from "../../../lib/tasklist.jsx";
import Task from "./Task";
+import socket from "../../../socket";
import "../../css/settings/settings.css";
import { Trans } from "react-i18next";
@@ -16,6 +17,18 @@ export default function Tasks() {
const [processing, setProcessing] = useState(false);
const [taskIntervals, setTaskIntervals] = useState([]);
const token = localStorage.getItem("token");
+ const [taskStateList, setTaskStateList] = useState();
+
+ useEffect(() => {
+ socket.on("task-list", (data) => {
+ if (typeof data === "object" && Array.isArray(data)) {
+ setTaskStateList(data);
+ }
+ });
+ return () => {
+ socket.off("task-list");
+ };
+ }, [taskStateList]);
async function executeTask(url) {
setProcessing(true);
@@ -33,6 +46,19 @@ export default function Tasks() {
setProcessing(false);
}
+ async function stopTask(task) {
+ await axios
+ .get(`/api/stopTask?task=${task}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+ }
+
async function updateTaskSettings(taskName, Interval) {
taskName = taskName.replace(/ /g, "");
@@ -100,15 +126,17 @@ export default function Tasks() {
{taskList.map((task) => (
-
- ))}
+ updateTask={updateTaskSettings}
+ onClick={executeTask}
+ stopTask={stopTask}
+ />
+ ))}