Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
56039bd
Add webhook management and scheduling functionality
BreizhHardware Apr 25, 2025
e3e3a16
Add webhooks settings component and integrate into settings page
BreizhHardware Apr 25, 2025
165927f
Update webhook component for improved user experience and error handling
BreizhHardware Apr 25, 2025
193b47c
Improve error messages and logging in webhook manager and scheduler a…
BreizhHardware Apr 26, 2025
20d100c
make loading show on initial page load to prevent flicker when data i…
CyferShepard Apr 26, 2025
27062b2
Merge branch 'CyferShepard:main' into main
BreizhHardware May 23, 2025
280fa89
feat(webhooks): add support for playback and media notification events
BreizhHardware May 23, 2025
eeada4f
feat(webhooks): add Discord webhook support and event notifications f…
BreizhHardware May 26, 2025
d9aba8a
Update src/pages/components/settings/webhooks.jsx
BreizhHardware May 26, 2025
247df5f
Update backend/routes/webhooks.js
BreizhHardware May 26, 2025
1f1a51f
Update backend/routes/webhooks.js
BreizhHardware May 26, 2025
b2e6a44
Update backend/routes/sync.js
BreizhHardware May 26, 2025
c3c7c68
Merge pull request #1 from BreizhHardware/dev
BreizhHardware May 26, 2025
2feef53
refactor(webhooks): improve Discord webhook handling and update notif…
BreizhHardware Jun 2, 2025
d79223b
Merge branch 'unstable' into main
CyferShepard Oct 5, 2025
6c15312
updated migrations to account for migrations from unstable branch
CyferShepard Oct 5, 2025
051acf5
Merge branch 'main' of https://github.com/BreizhHardware/Jellystat in…
CyferShepard Oct 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
418 changes: 418 additions & 0 deletions backend/classes/webhook-manager.js

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions backend/classes/webhook-scheduler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const cron = require('node-cron');
const WebhookManager = require('./webhook-manager');
const dbInstance = require('../db');

class WebhookScheduler {
constructor() {
this.webhookManager = new WebhookManager();
this.cronJobs = {};
this.loadScheduledWebhooks();
}

async loadScheduledWebhooks() {
try {
const webhooks = await this.webhookManager.getScheduledWebhooks();
if (webhooks) {
// Clean existing tasks
Object.values(this.cronJobs).forEach(job => job.stop());
this.cronJobs = {};

// Create new tasks
webhooks.forEach(webhook => {
if (webhook.schedule && cron.validate(webhook.schedule)) {
this.scheduleWebhook(webhook);
} else {
console.error(`[WEBHOOK] Invalid cron schedule for webhook ${webhook.id}: ${webhook.schedule}`);
}
});

console.log(`[WEBHOOK] Scheduled ${Object.keys(this.cronJobs).length} webhooks`);
} else {
console.log('[WEBHOOK] No scheduled webhooks found');
}
} catch (error) {
console.error('[WEBHOOK] Failed to load scheduled webhooks:', error);
}
}

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 () => {
console.log(`[WEBHOOK] Executing scheduled webhook: ${webhook.name}`);
await this.webhookManager.executeWebhook(webhook);
});

console.log(`[WEBHOOK] Webhook ${webhook.name} scheduled with cron: ${webhook.schedule}`);
} catch (error) {
console.error(`[WEBHOOK] Error scheduling webhook ${webhook.id}:`, error);
}
}

async refreshSchedule() {
await this.loadScheduledWebhooks();
await this.loadEventWebhooks();
}
}

module.exports = WebhookScheduler;
23 changes: 23 additions & 0 deletions backend/migrations/098_create_webhooks_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
exports.up = function (knex) {
return knex.schema.createTable("webhooks", (table) => {
table.increments("id").primary();
table.string("name").notNullable();
table.string("url").notNullable();
table.text("headers").defaultTo("{}");
table.text("payload").defaultTo("{}");
table.string("method").defaultTo("POST");
table.string("trigger_type").notNullable();
table.string("webhook_type").defaultTo("generic");
table.string("schedule").nullable();
table.string("event_type").nullable();
table.boolean("enabled").defaultTo(true);
table.timestamp("last_triggered").nullable();
table.boolean("retry_on_failure").defaultTo(false);
table.integer("max_retries").defaultTo(3);
table.timestamps(true, true);
});
};

exports.down = function (knex) {
return knex.schema.dropTable("webhooks");
};
24 changes: 19 additions & 5 deletions backend/routes/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -852,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 {
Expand All @@ -861,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();
Expand All @@ -870,18 +872,18 @@ 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 || [];

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
Expand Down Expand Up @@ -984,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;
Expand Down Expand Up @@ -1051,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 };
}
}

Expand Down
Loading