mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
418 lines
15 KiB
JavaScript
418 lines
15 KiB
JavaScript
const axios = require('axios');
|
|
const dbInstance = require('../db');
|
|
const EventEmitter = require('events');
|
|
|
|
class WebhookManager {
|
|
constructor() {
|
|
if (WebhookManager.instance) {
|
|
return WebhookManager.instance;
|
|
}
|
|
|
|
this.eventEmitter = new EventEmitter();
|
|
this.setupEventListeners();
|
|
WebhookManager.instance = this;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Adding event listeners for different events
|
|
this.eventEmitter.on('playback_started', async (data) => {
|
|
await this.triggerEventWebhooks('playback_started', 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
|
|
}
|
|
|
|
async getWebhooksByEventType(eventType) {
|
|
return await dbInstance.query(
|
|
'SELECT * FROM webhooks WHERE trigger_type = $1 AND event_type = $2 AND enabled = true',
|
|
['event', eventType]
|
|
).then(res => res.rows);
|
|
}
|
|
|
|
async getScheduledWebhooks() {
|
|
return await dbInstance.query(
|
|
'SELECT * FROM webhooks WHERE trigger_type = $1 AND enabled = true',
|
|
['scheduled']
|
|
).then(res => res.rows);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
async executeWebhook(webhook, data = {}) {
|
|
try {
|
|
let headers = {};
|
|
let payload = {};
|
|
|
|
const isDiscordWebhook = webhook.url.includes('discord.com/api/webhooks');
|
|
|
|
try {
|
|
headers = typeof webhook.headers === 'string'
|
|
? JSON.parse(webhook.headers || '{}')
|
|
: (webhook.headers || {});
|
|
|
|
payload = typeof webhook.payload === 'string'
|
|
? JSON.parse(webhook.payload || '{}')
|
|
: (webhook.payload || {});
|
|
} catch (e) {
|
|
console.error("[WEBHOOK] Error while parsing:", e);
|
|
return false;
|
|
}
|
|
|
|
if (isDiscordWebhook) {
|
|
console.log("[WEBHOOK] Webhook Discord detected");
|
|
|
|
await axios({
|
|
method: webhook.method || 'POST',
|
|
url: webhook.url,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
data: payload,
|
|
timeout: 10000
|
|
});
|
|
|
|
console.log(`[WEBHOOK] Discord webhook ${webhook.name} send successfully`);
|
|
} else {
|
|
const compiledPayload = this.compileTemplate(payload, data);
|
|
|
|
await axios({
|
|
method: webhook.method || 'POST',
|
|
url: webhook.url,
|
|
headers,
|
|
data: compiledPayload,
|
|
timeout: 10000
|
|
});
|
|
|
|
console.log(`[WEBHOOK] Webhook ${webhook.name} send successfully`);
|
|
}
|
|
|
|
//Update the last triggered timestamp
|
|
await dbInstance.query(
|
|
'UPDATE webhooks SET last_triggered = NOW() WHERE id = $1',
|
|
[webhook.id]
|
|
);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(`[WEBHOOK] Error triggering 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;
|
|
}
|
|
}
|
|
|
|
compileTemplate(template, data) {
|
|
if (typeof template === 'object') {
|
|
return Object.keys(template).reduce((result, key) => {
|
|
result[key] = this.compileTemplate(template[key], data);
|
|
return result;
|
|
}, {});
|
|
} else if (typeof template === 'string') {
|
|
// Replace {{variable}} with the corresponding value from data
|
|
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
const keys = path.trim().split('.');
|
|
let value = data;
|
|
|
|
for (const key of keys) {
|
|
if (value === undefined) return match;
|
|
value = value[key];
|
|
}
|
|
|
|
return value !== undefined ? value : match;
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
async getTopWatchedContent(contentType, period = 'month', limit = 5) {
|
|
// Calculate period start date
|
|
const today = new Date();
|
|
let startDate;
|
|
|
|
if (period === 'month') {
|
|
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
} else if (period === 'week') {
|
|
const day = today.getDay();
|
|
startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() - day - 7);
|
|
} else {
|
|
startDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
}
|
|
|
|
const formattedStartDate = startDate.toISOString().split('T')[0];
|
|
|
|
// SQL query to get top watched content
|
|
let query;
|
|
if (contentType === 'movie') {
|
|
query = `
|
|
SELECT
|
|
"NowPlayingItemName" as title,
|
|
COUNT(DISTINCT "UserId") as unique_viewers,
|
|
SUM("PlaybackDuration") / 60000 as total_minutes
|
|
FROM jf_playback_activity
|
|
WHERE "ActivityDateInserted" >= $1
|
|
AND "NowPlayingItemName" IS NOT NULL
|
|
AND "SeriesName" IS NULL
|
|
GROUP BY "NowPlayingItemName", "NowPlayingItemId"
|
|
ORDER BY total_minutes DESC
|
|
LIMIT $2
|
|
`;
|
|
} else if (contentType === 'series') {
|
|
query = `
|
|
SELECT
|
|
"SeriesName" as title,
|
|
COUNT(DISTINCT "UserId") as unique_viewers,
|
|
SUM("PlaybackDuration") / 60000 as total_minutes
|
|
FROM jf_playback_activity
|
|
WHERE "ActivityDateInserted" >= $1
|
|
AND "SeriesName" IS NOT NULL
|
|
GROUP BY "SeriesName"
|
|
ORDER BY total_minutes DESC
|
|
LIMIT $2
|
|
`;
|
|
}
|
|
|
|
try {
|
|
const result = await dbInstance.query(query, [formattedStartDate, limit]);
|
|
return result.rows || [];
|
|
} catch (error) {
|
|
console.error(`[WEBHOOK] SQL ERROR (${contentType}):`, error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getMonthlySummaryData() {
|
|
try {
|
|
// Get the top watched movies and series
|
|
const topMovies = await this.getTopWatchedContent('movie', 'month', 5);
|
|
const topSeries = await this.getTopWatchedContent('series', 'month', 5);
|
|
|
|
const prevMonth = new Date();
|
|
prevMonth.setMonth(prevMonth.getMonth() - 1);
|
|
const prevMonthStart = new Date(prevMonth.getFullYear(), prevMonth.getMonth(), 1);
|
|
const prevMonthEnd = new Date(prevMonth.getFullYear(), prevMonth.getMonth() + 1, 0);
|
|
|
|
const formattedStart = prevMonthStart.toISOString().split('T')[0];
|
|
const formattedEnd = prevMonthEnd.toISOString().split('T')[0];
|
|
|
|
// Get general statistics
|
|
const statsQuery = `
|
|
SELECT
|
|
COUNT(DISTINCT "UserId") as active_users,
|
|
COUNT(*) as total_plays,
|
|
SUM("PlaybackDuration") / 3600000 as total_hours
|
|
FROM jf_playback_activity
|
|
WHERE "ActivityDateInserted" BETWEEN $1 AND $2
|
|
`;
|
|
|
|
const statsResult = await dbInstance.query(statsQuery, [formattedStart, formattedEnd]);
|
|
const generalStats = statsResult.rows[0] || {
|
|
active_users: 0,
|
|
total_plays: 0,
|
|
total_hours: 0
|
|
};
|
|
|
|
return {
|
|
period: {
|
|
start: formattedStart,
|
|
end: formattedEnd,
|
|
name: prevMonth.toLocaleString('fr-FR', { month: 'long', year: 'numeric' })
|
|
},
|
|
topMovies,
|
|
topSeries,
|
|
stats: generalStats
|
|
};
|
|
} catch (error) {
|
|
console.error("[WEBHOOK] Error while getting data:", error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async triggerMonthlySummaryWebhook(webhookId) {
|
|
try {
|
|
// Get the webhook details
|
|
const result = await dbInstance.query(
|
|
'SELECT * FROM webhooks WHERE id = $1 AND enabled = true',
|
|
[webhookId]
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
console.error(`[WEBHOOK] Webhook ID ${webhookId} not found or disable`);
|
|
return false;
|
|
}
|
|
|
|
const webhook = result.rows[0];
|
|
|
|
// Generate the monthly summary data
|
|
try {
|
|
const data = await this.getMonthlySummaryData();
|
|
|
|
const moviesFields = data.topMovies.map((movie, index) => ({
|
|
name: `${index + 1}. ${movie.title}`,
|
|
value: `${Math.round(movie.total_minutes)} minutes • ${movie.unique_viewers} viewers`,
|
|
inline: false
|
|
}));
|
|
|
|
const seriesFields = data.topSeries.map((series, index) => ({
|
|
name: `${index + 1}. ${series.title}`,
|
|
value: `${Math.round(series.total_minutes)} minutes • ${series.unique_viewers} viewers`,
|
|
inline: false
|
|
}));
|
|
|
|
const monthlyPayload = {
|
|
content: `📊 **Monthly Report - ${data.period.name}**`,
|
|
embeds: [
|
|
{
|
|
title: "🎬 Most Watched Movies",
|
|
color: 15844367,
|
|
fields: moviesFields.length > 0 ? moviesFields : [{ name: "No data", value: "No movies watch this month" }]
|
|
},
|
|
{
|
|
title: "📺 Most Watched Series",
|
|
color: 5793266,
|
|
fields: seriesFields.length > 0 ? seriesFields : [{ name: "No data", value: "No Series watch this month" }]
|
|
},
|
|
{
|
|
title: "📈 General Statistics",
|
|
color: 5763719,
|
|
fields: [
|
|
{
|
|
name: "Active Users",
|
|
value: `${data.stats.active_users || 0}`,
|
|
inline: true
|
|
},
|
|
{
|
|
name: "Total Plays",
|
|
value: `${data.stats.total_plays || 0}`,
|
|
inline: true
|
|
},
|
|
{
|
|
name: "Total Hours Watched",
|
|
value: `${Math.round(data.stats.total_hours || 0)}`,
|
|
inline: true
|
|
}
|
|
],
|
|
footer: {
|
|
text: `Period: from ${new Date(data.period.start).toLocaleDateString('en-US')} to ${new Date(data.period.end).toLocaleDateString('en-US')}`
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
// Send the webhook
|
|
await axios({
|
|
method: webhook.method || 'POST',
|
|
url: webhook.url,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
data: monthlyPayload,
|
|
timeout: 10000
|
|
});
|
|
|
|
console.log(`[WEBHOOK] Monthly report webhook ${webhook.name} sent successfully`);
|
|
|
|
// Update the last triggered timestamp
|
|
await dbInstance.query(
|
|
'UPDATE webhooks SET last_triggered = NOW() WHERE id = $1',
|
|
[webhook.id]
|
|
);
|
|
|
|
return true;
|
|
} catch (dataError) {
|
|
console.error(`[WEBHOOK] Error while preparing the data:`, dataError.message);
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
console.error(`[WEBHOOK] Error while sending the monthly report:`, error.message);
|
|
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; |