mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Merge branch 'main' of https://github.com/BreizhHardware/Jellystat into pr/384
This commit is contained in:
11
.github/workflows/docker-image.yml
vendored
11
.github/workflows/docker-image.yml
vendored
@@ -39,12 +39,21 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: |
|
||||||
|
${{ steps.meta.outputs.tags }}
|
||||||
|
ghcr.io/${{ steps.meta.outputs.tags }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
9
.github/workflows/docker-latest.yml
vendored
9
.github/workflows/docker-latest.yml
vendored
@@ -44,6 +44,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -53,4 +60,6 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
cyfershepard/jellystat:latest
|
cyfershepard/jellystat:latest
|
||||||
cyfershepard/jellystat:${{ env.VERSION }}
|
cyfershepard/jellystat:${{ env.VERSION }}
|
||||||
|
ghcr.io/cyfershepard/jellystat:latest
|
||||||
|
ghcr.io/cyfershepard/jellystat:${{ env.VERSION }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
FROM node:slim AS builder
|
FROM node:lts-slim AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ COPY entry.sh ./
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 2: Create the production image
|
# Stage 2: Create the production image
|
||||||
FROM node:slim
|
FROM node:lts-slim
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -yqq --no-install-recommends wget && \
|
apt-get install -yqq --no-install-recommends wget && \
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
| POSTGRES_PASSWORD `REQUIRED` | `null` | `postgres` | Password that will be used in postgres database |
|
| POSTGRES_PASSWORD `REQUIRED` | `null` | `postgres` | Password that will be used in postgres database |
|
||||||
| POSTGRES_IP `REQUIRED` | `null` | `jellystat-db` or `192.168.0.5` | Hostname/IP of postgres instance |
|
| POSTGRES_IP `REQUIRED` | `null` | `jellystat-db` or `192.168.0.5` | Hostname/IP of postgres instance |
|
||||||
| POSTGRES_PORT `REQUIRED` | `null` | `5432` | Port Postgres is running on |
|
| POSTGRES_PORT `REQUIRED` | `null` | `5432` | Port Postgres is running on |
|
||||||
|
| POSTGRES_SSL_ENABLED | `null` | `true` | Enable SSL connections to Postgres
|
||||||
|
| POSTGRES_SSL_REJECT_UNAUTHORIZED | `null` | `false` | Verify Postgres SSL certificates when POSTGRES_SSL_ENABLED=true
|
||||||
| JS_LISTEN_IP | `0.0.0.0`| `0.0.0.0` or `::` | Enable listening on specific IP or `::` for IPv6 |
|
| JS_LISTEN_IP | `0.0.0.0`| `0.0.0.0` or `::` | Enable listening on specific IP or `::` for IPv6 |
|
||||||
| JWT_SECRET `REQUIRED` | `null` | `my-secret-jwt-key` | JWT Key to be used to encrypt JWT tokens for authentication |
|
| JWT_SECRET `REQUIRED` | `null` | `my-secret-jwt-key` | JWT Key to be used to encrypt JWT tokens for authentication |
|
||||||
| TZ `REQUIRED` | `null` | `Etc/UTC` | Server timezone (Can be found at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) |
|
| TZ `REQUIRED` | `null` | `Etc/UTC` | Server timezone (Can be found at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) |
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const configClass = require("./config");
|
const configClass = require("./config");
|
||||||
|
|
||||||
const moment = require("moment");
|
const dayjs = require("dayjs");
|
||||||
const Logging = require("./logging");
|
const Logging = require("./logging");
|
||||||
|
|
||||||
const taskstate = require("../logging/taskstate");
|
const taskstate = require("../logging/taskstate");
|
||||||
@@ -34,7 +34,7 @@ async function backup(refLog) {
|
|||||||
if (config.error) {
|
if (config.error) {
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed: Failed to get config" });
|
refLog.logData.push({ color: "red", Message: "Backup Failed: Failed to get config" });
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
||||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ async function backup(refLog) {
|
|||||||
// Get data from each table and append it to the backup file
|
// Get data from each table and append it to the backup file
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let now = moment();
|
let now = dayjs();
|
||||||
const backuppath = "./" + backupfolder;
|
const backuppath = "./" + backupfolder;
|
||||||
|
|
||||||
if (!fs.existsSync(backuppath)) {
|
if (!fs.existsSync(backuppath)) {
|
||||||
@@ -61,7 +61,7 @@ async function backup(refLog) {
|
|||||||
console.error("No write permissions for the folder:", backuppath);
|
console.error("No write permissions for the folder:", backuppath);
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed: No write permissions for the folder: " + backuppath });
|
refLog.logData.push({ color: "red", Message: "Backup Failed: No write permissions for the folder: " + backuppath });
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
||||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||||
await pool.end();
|
await pool.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -73,18 +73,18 @@ async function backup(refLog) {
|
|||||||
if (filteredTables.length === 0) {
|
if (filteredTables.length === 0) {
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed: No tables to backup" });
|
refLog.logData.push({ color: "red", Message: "Backup Failed: No tables to backup" });
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
refLog.logData.push({ color: "red", Message: "Backup Failed with errors" });
|
||||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||||
await pool.end();
|
await pool.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const backupPath = `../backup-data/backup_${now.format('yyyy-MM-DD HH-mm-ss')}.json`;
|
// const backupPath = `../backup-data/backup_${now.format('YYYY-MM-DD HH-mm-ss')}.json`;
|
||||||
const directoryPath = path.join(__dirname, "..", backupfolder, `backup_${now.format("yyyy-MM-DD HH-mm-ss")}.json`);
|
const directoryPath = path.join(__dirname, "..", backupfolder, `backup_${now.format("YYYY-MM-DD HH-mm-ss")}.json`);
|
||||||
refLog.logData.push({ color: "yellow", Message: "Begin Backup " + directoryPath });
|
refLog.logData.push({ color: "yellow", Message: "Begin Backup " + directoryPath });
|
||||||
const stream = fs.createWriteStream(directoryPath, { flags: "a" });
|
const stream = fs.createWriteStream(directoryPath, { flags: "a" });
|
||||||
stream.on("error", (error) => {
|
stream.on("error", async (error) => {
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed: " + error });
|
refLog.logData.push({ color: "red", Message: "Backup Failed: " + error });
|
||||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
const backup_data = [];
|
const backup_data = [];
|
||||||
@@ -152,7 +152,7 @@ async function backup(refLog) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
refLog.logData.push({ color: "red", Message: "Backup Failed: " + error });
|
refLog.logData.push({ color: "red", Message: "Backup Failed: " + error });
|
||||||
Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
await Logging.updateLog(refLog.uuid, refLog.logData, taskstate.FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
await pool.end();
|
await pool.end();
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ class EmbyAPI {
|
|||||||
|
|
||||||
//Functions
|
//Functions
|
||||||
|
|
||||||
async getUsers() {
|
async getUsers(refreshConfig = false) {
|
||||||
if (!this.configReady) {
|
if (!this.configReady || refreshConfig) {
|
||||||
const success = await this.#fetchConfig();
|
const success = await this.#fetchConfig();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return [];
|
return [];
|
||||||
@@ -133,9 +133,9 @@ class EmbyAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAdmins() {
|
async getAdmins(refreshConfig = false) {
|
||||||
try {
|
try {
|
||||||
const users = await this.getUsers();
|
const users = await this.getUsers(refreshConfig);
|
||||||
return users?.filter((user) => user.Policy.IsAdministrator) || [];
|
return users?.filter((user) => user.Policy.IsAdministrator) || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#errorHandler(error);
|
this.#errorHandler(error);
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ class JellyfinAPI {
|
|||||||
|
|
||||||
//Functions
|
//Functions
|
||||||
|
|
||||||
async getUsers() {
|
async getUsers(refreshConfig = false) {
|
||||||
if (!this.configReady) {
|
if (!this.configReady || refreshConfig) {
|
||||||
const success = await this.#fetchConfig();
|
const success = await this.#fetchConfig();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return [];
|
return [];
|
||||||
@@ -133,9 +133,9 @@ class JellyfinAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAdmins() {
|
async getAdmins(refreshConfig = false) {
|
||||||
try {
|
try {
|
||||||
const users = await this.getUsers();
|
const users = await this.getUsers(refreshConfig);
|
||||||
return users?.filter((user) => user.Policy.IsAdministrator) || [];
|
return users?.filter((user) => user.Policy.IsAdministrator) || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#errorHandler(error);
|
this.#errorHandler(error);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const moment = require("moment");
|
const dayjs = require("dayjs");
|
||||||
const taskstate = require("../logging/taskstate");
|
const taskstate = require("../logging/taskstate");
|
||||||
|
|
||||||
const { jf_logging_columns, jf_logging_mapping } = require("../models/jf_logging");
|
const { jf_logging_columns, jf_logging_mapping } = require("../models/jf_logging");
|
||||||
|
|
||||||
async function insertLog(uuid, triggertype, taskType) {
|
async function insertLog(uuid, triggertype, taskType) {
|
||||||
try {
|
try {
|
||||||
let startTime = moment();
|
let startTime = dayjs();
|
||||||
const log = {
|
const log = {
|
||||||
Id: uuid,
|
Id: uuid,
|
||||||
Name: taskType,
|
Name: taskType,
|
||||||
@@ -32,8 +32,8 @@ async function updateLog(uuid, data, taskstate) {
|
|||||||
if (task.length === 0) {
|
if (task.length === 0) {
|
||||||
console.log("Unable to find task to update");
|
console.log("Unable to find task to update");
|
||||||
} else {
|
} else {
|
||||||
let endtime = moment();
|
let endtime = dayjs();
|
||||||
let startTime = moment(task[0].TimeRun);
|
let startTime = dayjs(task[0].TimeRun);
|
||||||
let duration = endtime.diff(startTime, "seconds");
|
let duration = endtime.diff(startTime, "seconds");
|
||||||
const log = {
|
const log = {
|
||||||
Id: uuid,
|
Id: uuid,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class TaskManager {
|
|||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
console.error(`Worker ${task.name} stopped with exit code ${code}`);
|
console.error(`Worker ${task.name} stopped with exit code ${code}`);
|
||||||
}
|
}
|
||||||
if (onExit) {
|
if (code !== 0 && onExit) {
|
||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
delete this.tasks[task.name];
|
delete this.tasks[task.name];
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ class WebhookManager {
|
|||||||
await this.triggerEventWebhooks('playback_started', data);
|
await this.triggerEventWebhooks('playback_started', data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.on('user_login', async (data) => {
|
this.eventEmitter.on('playback_ended', async (data) => {
|
||||||
await this.triggerEventWebhooks('user_login', 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
|
// If needed, add more event listeners here
|
||||||
@@ -40,11 +44,33 @@ class WebhookManager {
|
|||||||
).then(res => res.rows);
|
).then(res => res.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
async triggerEventWebhooks(eventType, data) {
|
async triggerEventWebhooks(eventType, data = {}) {
|
||||||
const webhooks = await this.getWebhooksByEventType(eventType);
|
try {
|
||||||
|
const webhooks = await this.getWebhooksByEventType(eventType);
|
||||||
for (const webhook of webhooks) {
|
|
||||||
await this.executeWebhook(webhook, data);
|
if (webhooks.length === 0) {
|
||||||
|
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
|
||||||
|
|
||||||
|
const enrichedData = {
|
||||||
|
...data,
|
||||||
|
event: eventType,
|
||||||
|
triggeredAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const promises = webhooks.map(webhook => {
|
||||||
|
return this.executeWebhook(webhook, enrichedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +161,31 @@ class WebhookManager {
|
|||||||
return template;
|
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) {
|
emitEvent(eventType, data) {
|
||||||
this.eventEmitter.emit(eventType, data);
|
this.eventEmitter.emit(eventType, data);
|
||||||
}
|
}
|
||||||
@@ -340,6 +391,28 @@ class WebhookManager {
|
|||||||
return false;
|
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;
|
module.exports = WebhookManager;
|
||||||
@@ -35,6 +35,54 @@ class WebhookScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadEventWebhooks() {
|
||||||
|
try {
|
||||||
|
const eventWebhooks = await this.webhookManager.getEventWebhooks();
|
||||||
|
if (eventWebhooks && eventWebhooks.length > 0) {
|
||||||
|
this.eventWebhooks = {};
|
||||||
|
|
||||||
|
eventWebhooks.forEach(webhook => {
|
||||||
|
if (!this.eventWebhooks[webhook.eventType]) {
|
||||||
|
this.eventWebhooks[webhook.eventType] = [];
|
||||||
|
}
|
||||||
|
this.eventWebhooks[webhook.eventType].push(webhook);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[WEBHOOK] Loaded ${eventWebhooks.length} event-based webhooks`);
|
||||||
|
} else {
|
||||||
|
console.log('[WEBHOOK] No event-based webhooks found');
|
||||||
|
this.eventWebhooks = {};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WEBHOOK] Failed to load event-based webhooks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async triggerEvent(eventType, eventData = {}) {
|
||||||
|
try {
|
||||||
|
const webhooks = this.eventWebhooks[eventType] || [];
|
||||||
|
|
||||||
|
if (webhooks.length === 0) {
|
||||||
|
console.log(`[WEBHOOK] No webhooks registered for event: ${eventType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[WEBHOOK] Triggering ${webhooks.length} webhooks for event: ${eventType}`);
|
||||||
|
|
||||||
|
const promises = webhooks.map(webhook => {
|
||||||
|
return this.webhookManager.executeWebhook(webhook, {
|
||||||
|
event: eventType,
|
||||||
|
data: eventData,
|
||||||
|
triggeredAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error triggering webhooks for event ${eventType}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scheduleWebhook(webhook) {
|
scheduleWebhook(webhook) {
|
||||||
try {
|
try {
|
||||||
this.cronJobs[webhook.id] = cron.schedule(webhook.schedule, async () => {
|
this.cronJobs[webhook.id] = cron.schedule(webhook.schedule, async () => {
|
||||||
@@ -50,6 +98,7 @@ class WebhookScheduler {
|
|||||||
|
|
||||||
async refreshSchedule() {
|
async refreshSchedule() {
|
||||||
await this.loadScheduledWebhooks();
|
await this.loadScheduledWebhooks();
|
||||||
|
await this.loadEventWebhooks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ const _POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD;
|
|||||||
const _POSTGRES_IP = process.env.POSTGRES_IP;
|
const _POSTGRES_IP = process.env.POSTGRES_IP;
|
||||||
const _POSTGRES_PORT = process.env.POSTGRES_PORT;
|
const _POSTGRES_PORT = process.env.POSTGRES_PORT;
|
||||||
const _POSTGRES_DATABASE = process.env.POSTGRES_DB || 'jfstat';
|
const _POSTGRES_DATABASE = process.env.POSTGRES_DB || 'jfstat';
|
||||||
|
const _POSTGRES_SSL_REJECT_UNAUTHORIZED = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
|
||||||
|
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
host: _POSTGRES_IP,
|
host: _POSTGRES_IP,
|
||||||
user: _POSTGRES_USER,
|
user: _POSTGRES_USER,
|
||||||
password: _POSTGRES_PASSWORD,
|
password: _POSTGRES_PASSWORD,
|
||||||
port: _POSTGRES_PORT,
|
port: _POSTGRES_PORT,
|
||||||
|
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||||
|
? { ssl: { rejectUnauthorized: _POSTGRES_SSL_REJECT_UNAUTHORIZED } }
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
const createDatabase = async () => {
|
const createDatabase = async () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const _POSTGRES_PASSWORD = process.env.POSTGRES_PASSWORD;
|
|||||||
const _POSTGRES_IP = process.env.POSTGRES_IP;
|
const _POSTGRES_IP = process.env.POSTGRES_IP;
|
||||||
const _POSTGRES_PORT = process.env.POSTGRES_PORT;
|
const _POSTGRES_PORT = process.env.POSTGRES_PORT;
|
||||||
const _POSTGRES_DATABASE = process.env.POSTGRES_DB || "jfstat";
|
const _POSTGRES_DATABASE = process.env.POSTGRES_DB || "jfstat";
|
||||||
|
const _POSTGRES_SSL_REJECT_UNAUTHORIZED = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
|
||||||
|
|
||||||
if ([_POSTGRES_USER, _POSTGRES_PASSWORD, _POSTGRES_IP, _POSTGRES_PORT].includes(undefined)) {
|
if ([_POSTGRES_USER, _POSTGRES_PASSWORD, _POSTGRES_IP, _POSTGRES_PORT].includes(undefined)) {
|
||||||
console.log("Error: Postgres details not defined");
|
console.log("Error: Postgres details not defined");
|
||||||
@@ -22,6 +23,9 @@ const pool = new Pool({
|
|||||||
max: 20, // Maximum number of connections in the pool
|
max: 20, // Maximum number of connections in the pool
|
||||||
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
|
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
|
||||||
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
|
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
|
||||||
|
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||||
|
? { ssl: { rejectUnauthorized: _POSTGRES_SSL_REJECT_UNAUTHORIZED } }
|
||||||
|
: {})
|
||||||
});
|
});
|
||||||
|
|
||||||
pool.on("error", (err, client) => {
|
pool.on("error", (err, client) => {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ module.exports = {
|
|||||||
port:process.env.POSTGRES_PORT,
|
port:process.env.POSTGRES_PORT,
|
||||||
database: process.env.POSTGRES_DB || 'jfstat',
|
database: process.env.POSTGRES_DB || 'jfstat',
|
||||||
createDatabase: true,
|
createDatabase: true,
|
||||||
|
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||||
|
? { ssl: { rejectUnauthorized: process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true" } }
|
||||||
|
: {})
|
||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/migrations',
|
directory: __dirname + '/migrations',
|
||||||
@@ -39,6 +42,9 @@ module.exports = {
|
|||||||
port:process.env.POSTGRES_PORT,
|
port:process.env.POSTGRES_PORT,
|
||||||
database: process.env.POSTGRES_DB || 'jfstat',
|
database: process.env.POSTGRES_DB || 'jfstat',
|
||||||
createDatabase: true,
|
createDatabase: true,
|
||||||
|
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||||
|
? { ssl: { rejectUnauthorized: process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true" } }
|
||||||
|
: {})
|
||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: __dirname + '/migrations',
|
directory: __dirname + '/migrations',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const moment = require("moment");
|
const dayjs = require("dayjs");
|
||||||
const { randomUUID } = require("crypto");
|
const { randomUUID } = require("crypto");
|
||||||
|
|
||||||
const jf_activity_watchdog_columns = [
|
const jf_activity_watchdog_columns = [
|
||||||
@@ -45,7 +45,7 @@ const jf_activity_watchdog_mapping = (item) => ({
|
|||||||
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration : 0,
|
PlaybackDuration: item.PlaybackDuration !== undefined ? item.PlaybackDuration : 0,
|
||||||
PlayMethod: item.PlayState.PlayMethod,
|
PlayMethod: item.PlayState.PlayMethod,
|
||||||
ActivityDateInserted:
|
ActivityDateInserted:
|
||||||
item.ActivityDateInserted !== undefined ? item.ActivityDateInserted : moment().format("YYYY-MM-DD HH:mm:ss.SSSZ"),
|
item.ActivityDateInserted !== undefined ? item.ActivityDateInserted : dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ"),
|
||||||
MediaStreams: item.NowPlayingItem.MediaStreams ? item.NowPlayingItem.MediaStreams : null,
|
MediaStreams: item.NowPlayingItem.MediaStreams ? item.NowPlayingItem.MediaStreams : null,
|
||||||
TranscodingInfo: item.TranscodingInfo ? item.TranscodingInfo : null,
|
TranscodingInfo: item.TranscodingInfo ? item.TranscodingInfo : null,
|
||||||
PlayState: item.PlayState ? item.PlayState : null,
|
PlayState: item.PlayState ? item.PlayState : null,
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const jf_library_items_mapping = (item) => ({
|
|||||||
? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
|
? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
|
||||||
: null,
|
: null,
|
||||||
archived: false,
|
archived: false,
|
||||||
Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(filterInvalidGenres(item.Genres.map(titleCase))) : [],
|
Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(item.Genres.map(titleCase)) : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Utility function to title-case a string
|
// Utility function to title-case a string
|
||||||
@@ -62,53 +62,6 @@ function titleCase(str) {
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterInvalidGenres(genres) {
|
|
||||||
const validGenres = [
|
|
||||||
"Action",
|
|
||||||
"Adventure",
|
|
||||||
"Animated",
|
|
||||||
"Biography",
|
|
||||||
"Comedy",
|
|
||||||
"Crime",
|
|
||||||
"Dance",
|
|
||||||
"Disaster",
|
|
||||||
"Documentary",
|
|
||||||
"Drama",
|
|
||||||
"Erotic",
|
|
||||||
"Family",
|
|
||||||
"Fantasy",
|
|
||||||
"Found Footage",
|
|
||||||
"Historical",
|
|
||||||
"Horror",
|
|
||||||
"Independent",
|
|
||||||
"Legal",
|
|
||||||
"Live Action",
|
|
||||||
"Martial Arts",
|
|
||||||
"Musical",
|
|
||||||
"Mystery",
|
|
||||||
"Noir",
|
|
||||||
"Performance",
|
|
||||||
"Political",
|
|
||||||
"Romance",
|
|
||||||
"Satire",
|
|
||||||
"Science Fiction",
|
|
||||||
"Short",
|
|
||||||
"Silent",
|
|
||||||
"Slasher",
|
|
||||||
"Sports",
|
|
||||||
"Spy",
|
|
||||||
"Superhero",
|
|
||||||
"Supernatural",
|
|
||||||
"Suspense",
|
|
||||||
"Teen",
|
|
||||||
"Thriller",
|
|
||||||
"War",
|
|
||||||
"Western",
|
|
||||||
];
|
|
||||||
|
|
||||||
return genres.filter((genre) => validGenres.map((g) => g.toLowerCase()).includes(genre.toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
jf_library_items_columns,
|
jf_library_items_columns,
|
||||||
jf_library_items_mapping,
|
jf_library_items_mapping,
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
////////////////////////// pn delete move to playback
|
////////////////////////// pn delete move to playback
|
||||||
const columnsPlaybackReporting = [
|
const columnsPlaybackReporting = [
|
||||||
"rowid",
|
"rowid",
|
||||||
"DateCreated",
|
"DateCreated",
|
||||||
"UserId",
|
"UserId",
|
||||||
"ItemId",
|
"ItemId",
|
||||||
"ItemType",
|
"ItemType",
|
||||||
"ItemName",
|
"ItemName",
|
||||||
"PlaybackMethod",
|
"PlaybackMethod",
|
||||||
"ClientName",
|
"ClientName",
|
||||||
"DeviceName",
|
"DeviceName",
|
||||||
"PlayDuration",
|
"PlayDuration",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mappingPlaybackReporting = (item) => {
|
||||||
|
let duration = item[9];
|
||||||
|
|
||||||
const mappingPlaybackReporting = (item) => ({
|
if (duration === null || duration === undefined || duration < 0) {
|
||||||
rowid:item[0] ,
|
duration = 0;
|
||||||
DateCreated:item[1] ,
|
}
|
||||||
UserId:item[2] ,
|
|
||||||
ItemId:item[3] ,
|
|
||||||
ItemType:item[4] ,
|
|
||||||
ItemName:item[5] ,
|
|
||||||
PlaybackMethod:item[6] ,
|
|
||||||
ClientName:item[7] ,
|
|
||||||
DeviceName:item[8] ,
|
|
||||||
PlayDuration:item[9] ,
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
return {
|
||||||
columnsPlaybackReporting,
|
rowid: item[0],
|
||||||
mappingPlaybackReporting,
|
DateCreated: item[1],
|
||||||
};
|
UserId: item[2],
|
||||||
|
ItemId: item[3],
|
||||||
|
ItemType: item[4],
|
||||||
|
ItemName: item[5],
|
||||||
|
PlaybackMethod: item[6],
|
||||||
|
ClientName: item[7],
|
||||||
|
DeviceName: item[8],
|
||||||
|
PlayDuration: duration,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
columnsPlaybackReporting,
|
||||||
|
mappingPlaybackReporting,
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ const configClass = require("../classes/config");
|
|||||||
const { checkForUpdates } = require("../version-control");
|
const { checkForUpdates } = require("../version-control");
|
||||||
const API = require("../classes/api-loader");
|
const API = require("../classes/api-loader");
|
||||||
const { sendUpdate } = require("../ws");
|
const { sendUpdate } = require("../ws");
|
||||||
const moment = require("moment");
|
|
||||||
const { tables } = require("../global/backup_tables");
|
const { tables } = require("../global/backup_tables");
|
||||||
const TaskScheduler = require("../classes/task-scheduler-singleton");
|
const TaskScheduler = require("../classes/task-scheduler-singleton");
|
||||||
const TaskManager = require("../classes/task-manager-singleton.js");
|
const TaskManager = require("../classes/task-manager-singleton.js");
|
||||||
|
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const customParseFormat = require("dayjs/plugin/customParseFormat");
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
//consts
|
//consts
|
||||||
@@ -329,11 +332,11 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
|||||||
|
|
||||||
let lastSynctedItemDate;
|
let lastSynctedItemDate;
|
||||||
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
|
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
|
||||||
lastSynctedItemDate = moment(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
lastSynctedItemDate = dayjs(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (episodes.length > 0 && episodes[0].DateCreated !== undefined && episodes[0].DateCreated !== null) {
|
if (episodes.length > 0 && episodes[0].DateCreated !== undefined && episodes[0].DateCreated !== null) {
|
||||||
const newLastSynctedItemDate = moment(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
const newLastSynctedItemDate = dayjs(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
|
|
||||||
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
|
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
|
||||||
lastSynctedItemDate = newLastSynctedItemDate;
|
lastSynctedItemDate = newLastSynctedItemDate;
|
||||||
@@ -342,7 +345,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
|||||||
|
|
||||||
if (lastSynctedItemDate !== undefined) {
|
if (lastSynctedItemDate !== undefined) {
|
||||||
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
|
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
|
||||||
moment(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
dayjs(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +357,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
|||||||
const recentlyAdded = [...recentlyAddedFromJellystatMapped, ...filteredDbRows];
|
const recentlyAdded = [...recentlyAddedFromJellystatMapped, ...filteredDbRows];
|
||||||
// Sort recentlyAdded by DateCreated in descending order
|
// Sort recentlyAdded by DateCreated in descending order
|
||||||
recentlyAdded.sort(
|
recentlyAdded.sort(
|
||||||
(a, b) => moment(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - moment(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
|
(a, b) => dayjs(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - dayjs(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
|
||||||
);
|
);
|
||||||
|
|
||||||
res.send(recentlyAdded);
|
res.send(recentlyAdded);
|
||||||
@@ -383,11 +386,11 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
|||||||
);
|
);
|
||||||
let lastSynctedItemDate;
|
let lastSynctedItemDate;
|
||||||
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
|
if (items.length > 0 && items[0].DateCreated !== undefined && items[0].DateCreated !== null) {
|
||||||
lastSynctedItemDate = moment(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
lastSynctedItemDate = dayjs(items[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (episodes.length > 0 && episodes[0].DateCreated !== undefined && episodes[0].DateCreated !== null) {
|
if (episodes.length > 0 && episodes[0].DateCreated !== undefined && episodes[0].DateCreated !== null) {
|
||||||
const newLastSynctedItemDate = moment(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
const newLastSynctedItemDate = dayjs(episodes[0].DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
|
|
||||||
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
|
if (lastSynctedItemDate === undefined || newLastSynctedItemDate.isAfter(lastSynctedItemDate)) {
|
||||||
lastSynctedItemDate = newLastSynctedItemDate;
|
lastSynctedItemDate = newLastSynctedItemDate;
|
||||||
@@ -396,7 +399,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
|||||||
|
|
||||||
if (lastSynctedItemDate !== undefined) {
|
if (lastSynctedItemDate !== undefined) {
|
||||||
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
|
recentlyAddedFromJellystatMapped = recentlyAddedFromJellystatMapped.filter((item) =>
|
||||||
moment(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
dayjs(item.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ").isAfter(lastSynctedItemDate)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +417,7 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
|||||||
|
|
||||||
// Sort recentlyAdded by DateCreated in descending order
|
// Sort recentlyAdded by DateCreated in descending order
|
||||||
recentlyAdded.sort(
|
recentlyAdded.sort(
|
||||||
(a, b) => moment(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - moment(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
|
(a, b) => dayjs(b.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ") - dayjs(a.DateCreated, "YYYY-MM-DD HH:mm:ss.SSSZ")
|
||||||
);
|
);
|
||||||
|
|
||||||
res.send(recentlyAdded);
|
res.send(recentlyAdded);
|
||||||
@@ -463,7 +466,24 @@ router.post("/setconfig", async (req, res) => {
|
|||||||
|
|
||||||
settings.ServerID = systemInfo?.Id || null;
|
settings.ServerID = systemInfo?.Id || null;
|
||||||
|
|
||||||
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
|
const query = 'UPDATE app_config SET settings=$1 where "ID"=1';
|
||||||
|
|
||||||
|
await db.query(query, [settings]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const admins = await API.getAdmins(true);
|
||||||
|
const preferredAdmin = await new configClass().getPreferedAdmin();
|
||||||
|
if (admins && admins.length > 0 && preferredAdmin && !admins.map((item) => item.Id).includes(preferredAdmin)) {
|
||||||
|
const newAdmin = admins[0];
|
||||||
|
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
|
||||||
|
|
||||||
|
if (settingsjson.length > 0) {
|
||||||
|
const settings = settingsjson[0].settings || {};
|
||||||
|
|
||||||
|
settings.preferred_admin = { userid: newAdmin.Id, username: newAdmin.Name };
|
||||||
|
|
||||||
|
const query = 'UPDATE app_config SET settings=$1 where "ID"=1';
|
||||||
|
|
||||||
await db.query(query, [settings]);
|
await db.query(query, [settings]);
|
||||||
}
|
}
|
||||||
@@ -892,6 +912,83 @@ router.post("/setTaskSettings", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get Activity Monitor Polling Settings
|
||||||
|
router.get("/getActivityMonitorSettings", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
|
||||||
|
|
||||||
|
if (settingsjson.length > 0) {
|
||||||
|
const settings = settingsjson[0].settings || {};
|
||||||
|
console.log(settings);
|
||||||
|
const pollingSettings = settings.ActivityMonitorPolling || {
|
||||||
|
activeSessionsInterval: 1000,
|
||||||
|
idleInterval: 5000
|
||||||
|
};
|
||||||
|
res.send(pollingSettings);
|
||||||
|
} else {
|
||||||
|
res.status(404);
|
||||||
|
res.send({ error: "Settings Not Found" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(503);
|
||||||
|
res.send({ error: "Error: " + error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set Activity Monitor Polling Settings
|
||||||
|
router.post("/setActivityMonitorSettings", async (req, res) => {
|
||||||
|
const { activeSessionsInterval, idleInterval } = req.body;
|
||||||
|
|
||||||
|
if (activeSessionsInterval === undefined || idleInterval === undefined) {
|
||||||
|
res.status(400);
|
||||||
|
res.send("activeSessionsInterval and idleInterval are required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(activeSessionsInterval) || activeSessionsInterval <= 0) {
|
||||||
|
res.status(400);
|
||||||
|
res.send("A valid activeSessionsInterval(int) which is > 0 milliseconds is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(idleInterval) || idleInterval <= 0) {
|
||||||
|
res.status(400);
|
||||||
|
res.send("A valid idleInterval(int) which is > 0 milliseconds is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSessionsInterval > idleInterval) {
|
||||||
|
res.status(400);
|
||||||
|
res.send("activeSessionsInterval should be <= idleInterval for optimal performance");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settingsjson = await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
|
||||||
|
|
||||||
|
if (settingsjson.length > 0) {
|
||||||
|
const settings = settingsjson[0].settings || {};
|
||||||
|
|
||||||
|
settings.ActivityMonitorPolling = {
|
||||||
|
activeSessionsInterval: activeSessionsInterval,
|
||||||
|
idleInterval: idleInterval
|
||||||
|
};
|
||||||
|
|
||||||
|
let query = 'UPDATE app_config SET settings=$1 where "ID"=1';
|
||||||
|
await db.query(query, [settings]);
|
||||||
|
|
||||||
|
res.status(200);
|
||||||
|
res.send(settings.ActivityMonitorPolling);
|
||||||
|
} else {
|
||||||
|
res.status(404);
|
||||||
|
res.send({ error: "Settings Not Found" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(503);
|
||||||
|
res.send({ error: "Error: " + error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Jellystat functions
|
//Jellystat functions
|
||||||
router.get("/CheckForUpdates", async (req, res) => {
|
router.get("/CheckForUpdates", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,16 +20,23 @@ router.post("/login", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
if (!username || !password || password === CryptoJS.SHA3("").toString()) {
|
const query = "SELECT * FROM app_config";
|
||||||
|
const { rows: login } = await db.query(query);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!username || !password || password === CryptoJS.SHA3("").toString()) &&
|
||||||
|
login.length > 0 &&
|
||||||
|
login[0].REQUIRE_LOGIN == true
|
||||||
|
) {
|
||||||
res.sendStatus(401);
|
res.sendStatus(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = 'SELECT * FROM app_config WHERE ("APP_USER" = $1 AND "APP_PASSWORD" = $2) OR "REQUIRE_LOGIN" = false';
|
const loginUser = login.filter(
|
||||||
const values = [username, password];
|
(user) => (user.APP_USER === username && user.APP_PASSWORD === password) || user.REQUIRE_LOGIN == false
|
||||||
const { rows: login } = await db.query(query, values);
|
);
|
||||||
|
|
||||||
if (login.length > 0 || (username === JS_USER && password === CryptoJS.SHA3(JS_PASSWORD).toString())) {
|
if (loginUser.length > 0 || (username === JS_USER && password === CryptoJS.SHA3(JS_PASSWORD).toString())) {
|
||||||
const user = { id: 1, username: username };
|
const user = { id: 1, username: username };
|
||||||
|
|
||||||
jwt.sign({ user }, JWT_SECRET, (err, token) => {
|
jwt.sign({ user }, JWT_SECRET, (err, token) => {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const postgresPassword = process.env.POSTGRES_PASSWORD;
|
|||||||
const postgresIp = process.env.POSTGRES_IP;
|
const postgresIp = process.env.POSTGRES_IP;
|
||||||
const postgresPort = process.env.POSTGRES_PORT;
|
const postgresPort = process.env.POSTGRES_PORT;
|
||||||
const postgresDatabase = process.env.POSTGRES_DB || "jfstat";
|
const postgresDatabase = process.env.POSTGRES_DB || "jfstat";
|
||||||
|
const postgresSslRejectUnauthorized = process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === undefined ? true : process.env.POSTGRES_SSL_REJECT_UNAUTHORIZED === "true";
|
||||||
|
|
||||||
const backupfolder = "backup-data";
|
const backupfolder = "backup-data";
|
||||||
|
|
||||||
// Restore function
|
// Restore function
|
||||||
@@ -52,6 +54,9 @@ async function restore(file, refLog) {
|
|||||||
host: postgresIp,
|
host: postgresIp,
|
||||||
port: postgresPort,
|
port: postgresPort,
|
||||||
database: postgresDatabase,
|
database: postgresDatabase,
|
||||||
|
...(process.env.POSTGRES_SSL_ENABLED === "true"
|
||||||
|
? { ssl: { rejectUnauthorized: postgresSslRejectUnauthorized } }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const backupPath = file;
|
const backupPath = file;
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ router.get("/getSessions", async (req, res) => {
|
|||||||
|
|
||||||
router.get("/getAdminUsers", async (req, res) => {
|
router.get("/getAdminUsers", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const adminUser = await API.getAdmins();
|
const adminUser = await API.getAdmins(true);
|
||||||
res.send(adminUser);
|
res.send(adminUser);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(503);
|
res.status(503);
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
const dbHelper = require("../classes/db-helper");
|
const dbHelper = require("../classes/db-helper");
|
||||||
const moment = require("moment");
|
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const customParseFormat = require("dayjs/plugin/customParseFormat");
|
||||||
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -11,8 +14,8 @@ function countOverlapsPerHour(records) {
|
|||||||
const hourCounts = {};
|
const hourCounts = {};
|
||||||
|
|
||||||
records.forEach((record) => {
|
records.forEach((record) => {
|
||||||
const start = moment(record.StartTime).subtract(1, "hour");
|
const start = dayjs(record.StartTime).subtract(1, "hour");
|
||||||
const end = moment(record.EndTime).add(1, "hour");
|
const end = dayjs(record.EndTime).add(1, "hour");
|
||||||
|
|
||||||
// Iterate through each hour from start to end
|
// Iterate through each hour from start to end
|
||||||
for (let hour = start.clone().startOf("hour"); hour.isBefore(end); hour.add(1, "hour")) {
|
for (let hour = start.clone().startOf("hour"); hour.isBefore(end); hour.add(1, "hour")) {
|
||||||
@@ -289,12 +292,12 @@ router.post("/getLibraryItemsWithStats", async (req, res) => {
|
|||||||
|
|
||||||
router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let { libraryid, startDate, endDate = moment(), hours = 24 } = req.body;
|
let { libraryid, startDate, endDate = dayjs(), hours = 24 } = req.body;
|
||||||
|
|
||||||
// Validate startDate and endDate using moment
|
// Validate startDate and endDate using dayjs
|
||||||
if (
|
if (
|
||||||
startDate !== undefined &&
|
startDate !== undefined &&
|
||||||
(!moment(startDate, moment.ISO_8601, true).isValid() || !moment(endDate, moment.ISO_8601, true).isValid())
|
(!dayjs(startDate, "YYYY-MM-DDTHH:mm:ss.SSSZ", true).isValid() || !dayjs(endDate, "YYYY-MM-DDTHH:mm:ss.SSSZ", true).isValid())
|
||||||
) {
|
) {
|
||||||
return res.status(400).send({ error: "Invalid date format" });
|
return res.status(400).send({ error: "Invalid date format" });
|
||||||
}
|
}
|
||||||
@@ -308,7 +311,7 @@ router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (startDate === undefined) {
|
if (startDate === undefined) {
|
||||||
startDate = moment(endDate).subtract(hours, "hour").format("YYYY-MM-DD HH:mm:ss");
|
startDate = dayjs(endDate).subtract(hours, "hour").format("YYYY-MM-DD HH:mm:ss");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
@@ -336,8 +339,8 @@ router.post("/getLibraryItemsPlayMethodStats", async (req, res) => {
|
|||||||
NowPlayingItemName: item.NowPlayingItemName,
|
NowPlayingItemName: item.NowPlayingItemName,
|
||||||
EpisodeId: item.EpisodeId || null,
|
EpisodeId: item.EpisodeId || null,
|
||||||
SeasonId: item.SeasonId || null,
|
SeasonId: item.SeasonId || null,
|
||||||
StartTime: moment(item.ActivityDateInserted).subtract(item.PlaybackDuration, "seconds").format("YYYY-MM-DD HH:mm:ss"),
|
StartTime: dayjs(item.ActivityDateInserted).subtract(item.PlaybackDuration, "seconds").format("YYYY-MM-DD HH:mm:ss"),
|
||||||
EndTime: moment(item.ActivityDateInserted).format("YYYY-MM-DD HH:mm:ss"),
|
EndTime: dayjs(item.ActivityDateInserted).format("YYYY-MM-DD HH:mm:ss"),
|
||||||
PlaybackDuration: item.PlaybackDuration,
|
PlaybackDuration: item.PlaybackDuration,
|
||||||
PlayMethod: item.PlayMethod,
|
PlayMethod: item.PlayMethod,
|
||||||
TranscodedVideo: item.TranscodingInfo?.IsVideoDirect || false,
|
TranscodedVideo: item.TranscodingInfo?.IsVideoDirect || false,
|
||||||
@@ -407,9 +410,9 @@ router.post("/getLibraryLastPlayed", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/getViewsOverTime", async (req, res) => {
|
router.get("/getViewsOverTime", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { days } = req.body;
|
const { days } = req.query;
|
||||||
let _days = days;
|
let _days = days;
|
||||||
if (days === undefined) {
|
if (days === undefined) {
|
||||||
_days = 30;
|
_days = 30;
|
||||||
@@ -423,6 +426,7 @@ router.post("/getViewsOverTime", async (req, res) => {
|
|||||||
stats.forEach((item) => {
|
stats.forEach((item) => {
|
||||||
const library = item.Library;
|
const library = item.Library;
|
||||||
const count = item.Count;
|
const count = item.Count;
|
||||||
|
const duration = item.Duration;
|
||||||
const date = new Date(item.Date).toLocaleDateString("en-US", {
|
const date = new Date(item.Date).toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
@@ -435,7 +439,7 @@ router.post("/getViewsOverTime", async (req, res) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reorganizedData[date] = { ...reorganizedData[date], [library]: count };
|
reorganizedData[date] = { ...reorganizedData[date], [library]: { count, duration } };
|
||||||
});
|
});
|
||||||
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
||||||
res.send(finalData);
|
res.send(finalData);
|
||||||
@@ -446,9 +450,9 @@ router.post("/getViewsOverTime", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/getViewsByDays", async (req, res) => {
|
router.get("/getViewsByDays", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { days } = req.body;
|
const { days } = req.query;
|
||||||
let _days = days;
|
let _days = days;
|
||||||
if (days === undefined) {
|
if (days === undefined) {
|
||||||
_days = 30;
|
_days = 30;
|
||||||
@@ -462,6 +466,7 @@ router.post("/getViewsByDays", async (req, res) => {
|
|||||||
stats.forEach((item) => {
|
stats.forEach((item) => {
|
||||||
const library = item.Library;
|
const library = item.Library;
|
||||||
const count = item.Count;
|
const count = item.Count;
|
||||||
|
const duration = item.Duration;
|
||||||
const day = item.Day;
|
const day = item.Day;
|
||||||
|
|
||||||
if (!reorganizedData[day]) {
|
if (!reorganizedData[day]) {
|
||||||
@@ -470,7 +475,7 @@ router.post("/getViewsByDays", async (req, res) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reorganizedData[day] = { ...reorganizedData[day], [library]: count };
|
reorganizedData[day] = { ...reorganizedData[day], [library]: { count, duration } };
|
||||||
});
|
});
|
||||||
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
||||||
res.send(finalData);
|
res.send(finalData);
|
||||||
@@ -481,9 +486,9 @@ router.post("/getViewsByDays", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/getViewsByHour", async (req, res) => {
|
router.get("/getViewsByHour", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { days } = req.body;
|
const { days } = req.query;
|
||||||
let _days = days;
|
let _days = days;
|
||||||
if (days === undefined) {
|
if (days === undefined) {
|
||||||
_days = 30;
|
_days = 30;
|
||||||
@@ -497,6 +502,7 @@ router.post("/getViewsByHour", async (req, res) => {
|
|||||||
stats.forEach((item) => {
|
stats.forEach((item) => {
|
||||||
const library = item.Library;
|
const library = item.Library;
|
||||||
const count = item.Count;
|
const count = item.Count;
|
||||||
|
const duration = item.Duration;
|
||||||
const hour = item.Hour;
|
const hour = item.Hour;
|
||||||
|
|
||||||
if (!reorganizedData[hour]) {
|
if (!reorganizedData[hour]) {
|
||||||
@@ -505,7 +511,7 @@ router.post("/getViewsByHour", async (req, res) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reorganizedData[hour] = { ...reorganizedData[hour], [library]: count };
|
reorganizedData[hour] = { ...reorganizedData[hour], [library]: { count, duration } };
|
||||||
});
|
});
|
||||||
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
const finalData = { libraries: libraries, stats: Object.values(reorganizedData) };
|
||||||
res.send(finalData);
|
res.send(finalData);
|
||||||
@@ -516,6 +522,41 @@ router.post("/getViewsByHour", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/getViewsByLibraryType", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { days = 30 } = req.query;
|
||||||
|
|
||||||
|
const { rows } = await db.query(`
|
||||||
|
SELECT COALESCE(i."Type", 'Other') AS type, COUNT(a."NowPlayingItemId") AS count
|
||||||
|
FROM jf_playback_activity a LEFT JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
||||||
|
WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST($1 || ' days' as INTERVAL) AND NOW()
|
||||||
|
GROUP BY i."Type"
|
||||||
|
`, [days]);
|
||||||
|
|
||||||
|
const supportedTypes = new Set(["Audio", "Movie", "Series", "Other"]);
|
||||||
|
/** @type {Map<string, number>} */
|
||||||
|
const reorganizedData = new Map();
|
||||||
|
|
||||||
|
rows.forEach((item) => {
|
||||||
|
const { type, count } = item;
|
||||||
|
|
||||||
|
if (!supportedTypes.has(type)) return;
|
||||||
|
reorganizedData.set(type, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
supportedTypes.forEach((type) => {
|
||||||
|
if (reorganizedData.has(type)) return;
|
||||||
|
reorganizedData.set(type, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(Object.fromEntries(reorganizedData));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(503);
|
||||||
|
res.send(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/getGenreUserStats", async (req, res) => {
|
router.get("/getGenreUserStats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { size = 50, page = 1, userid } = req.query;
|
const { size = 50, page = 1, userid } = req.query;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
const moment = require("moment");
|
const dayjs = require("dayjs");
|
||||||
const { randomUUID } = require("crypto");
|
const { randomUUID } = require("crypto");
|
||||||
|
|
||||||
const { sendUpdate } = require("../ws");
|
const { sendUpdate } = require("../ws");
|
||||||
@@ -39,13 +39,41 @@ function getErrorLineNumber(error) {
|
|||||||
return lineNumber;
|
return lineNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeNullBytes(obj) {
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
// Remove various forms of null bytes and control characters that cause Unicode escape sequence errors
|
||||||
|
return obj
|
||||||
|
.replace(/\u0000/g, '') // Remove null bytes
|
||||||
|
.replace(/\\u0000/g, '') // Remove escaped null bytes
|
||||||
|
.replace(/\x00/g, '') // Remove hex null bytes
|
||||||
|
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Remove all control characters
|
||||||
|
.trim(); // Remove leading/trailing whitespace
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(sanitizeNullBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
const sanitized = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
sanitized[key] = sanitizeNullBytes(value);
|
||||||
|
}
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
class sync {
|
class sync {
|
||||||
async getExistingIDsforTable(tablename) {
|
async getExistingIDsforTable(tablename) {
|
||||||
return await db.query(`SELECT "Id" FROM ${tablename}`).then((res) => res.rows.map((row) => row.Id));
|
return await db.query(`SELECT "Id" FROM ${tablename}`).then((res) => res.rows.map((row) => row.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async insertData(tablename, dataToInsert, column_mappings) {
|
async insertData(tablename, dataToInsert, column_mappings) {
|
||||||
let result = await db.insertBulk(tablename, dataToInsert, column_mappings);
|
const sanitizedData = sanitizeNullBytes(dataToInsert);
|
||||||
|
|
||||||
|
let result = await db.insertBulk(tablename, sanitizedData, column_mappings);
|
||||||
if (result.Result === "SUCCESS") {
|
if (result.Result === "SUCCESS") {
|
||||||
// syncTask.loggedData.push({ color: "dodgerblue", Message: dataToInsert.length + " Rows Inserted." });
|
// syncTask.loggedData.push({ color: "dodgerblue", Message: dataToInsert.length + " Rows Inserted." });
|
||||||
} else {
|
} else {
|
||||||
@@ -395,12 +423,13 @@ async function removeOrphanedData() {
|
|||||||
syncTask.loggedData.push({ color: "yellow", Message: "Removing Orphaned FileInfo/Episode/Season Records" });
|
syncTask.loggedData.push({ color: "yellow", Message: "Removing Orphaned FileInfo/Episode/Season Records" });
|
||||||
|
|
||||||
await db.query("CALL jd_remove_orphaned_data()");
|
await db.query("CALL jd_remove_orphaned_data()");
|
||||||
const archived_items = await db
|
const archived_items_query = `select i."Id" from jf_library_items i join jf_library_seasons s on s."SeriesId"=i."Id" and s.archived=false where i.archived=true and i."Type"='Series'
|
||||||
.query(`select "Id" from jf_library_items where archived=true and "Type"='Series'`)
|
union
|
||||||
.then((res) => res.rows.map((row) => row.Id));
|
select i."Id" from jf_library_items i join jf_library_episodes e on e."SeriesId"=i."Id" and e.archived=false where i.archived=true and i."Type"='Series'
|
||||||
const archived_seasons = await db
|
`;
|
||||||
.query(`select "Id" from jf_library_seasons where archived=true`)
|
const archived_items = await db.query(archived_items_query).then((res) => res.rows.map((row) => row.Id));
|
||||||
.then((res) => res.rows.map((row) => row.Id));
|
const archived_seasons_query = `select s."Id" from jf_library_seasons s join jf_library_episodes e on e."SeasonId"=s."Id" and e.archived=false where s.archived=true`;
|
||||||
|
const archived_seasons = await db.query(archived_seasons_query).then((res) => res.rows.map((row) => row.Id));
|
||||||
if (!(await _sync.updateSingleFieldOnDB("jf_library_seasons", archived_items, "archived", true, "SeriesId"))) {
|
if (!(await _sync.updateSingleFieldOnDB("jf_library_seasons", archived_items, "archived", true, "SeriesId"))) {
|
||||||
syncTask.loggedData.push({ color: "red", Message: "Error archiving library seasons" });
|
syncTask.loggedData.push({ color: "red", Message: "Error archiving library seasons" });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
@@ -529,13 +558,13 @@ async function syncPlaybackPluginData() {
|
|||||||
let query = `SELECT rowid, * FROM PlaybackActivity`;
|
let query = `SELECT rowid, * FROM PlaybackActivity`;
|
||||||
|
|
||||||
if (OldestPlaybackActivity && NewestPlaybackActivity) {
|
if (OldestPlaybackActivity && NewestPlaybackActivity) {
|
||||||
const formattedDateTimeOld = moment(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
const formattedDateTimeOld = dayjs(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||||
const formattedDateTimeNew = moment(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
const formattedDateTimeNew = dayjs(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||||
query = query + ` WHERE (DateCreated < '${formattedDateTimeOld}' or DateCreated > '${formattedDateTimeNew}')`;
|
query = query + ` WHERE (DateCreated < '${formattedDateTimeOld}' or DateCreated > '${formattedDateTimeNew}')`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OldestPlaybackActivity && !NewestPlaybackActivity) {
|
if (OldestPlaybackActivity && !NewestPlaybackActivity) {
|
||||||
const formattedDateTimeOld = moment(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
const formattedDateTimeOld = dayjs(OldestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||||
query = query + ` WHERE DateCreated < '${formattedDateTimeOld}'`;
|
query = query + ` WHERE DateCreated < '${formattedDateTimeOld}'`;
|
||||||
if (MaxPlaybackReportingPluginID) {
|
if (MaxPlaybackReportingPluginID) {
|
||||||
query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
|
query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
|
||||||
@@ -543,7 +572,7 @@ async function syncPlaybackPluginData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!OldestPlaybackActivity && NewestPlaybackActivity) {
|
if (!OldestPlaybackActivity && NewestPlaybackActivity) {
|
||||||
const formattedDateTimeNew = moment(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
const formattedDateTimeNew = dayjs(NewestPlaybackActivity).format("YYYY-MM-DD HH:mm:ss");
|
||||||
query = query + ` WHERE DateCreated > '${formattedDateTimeNew}'`;
|
query = query + ` WHERE DateCreated > '${formattedDateTimeNew}'`;
|
||||||
if (MaxPlaybackReportingPluginID) {
|
if (MaxPlaybackReportingPluginID) {
|
||||||
query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
|
query = query + ` AND rowid > ${MaxPlaybackReportingPluginID}`;
|
||||||
@@ -823,6 +852,8 @@ async function partialSync(triggertype) {
|
|||||||
const config = await new configClass().getConfig();
|
const config = await new configClass().getConfig();
|
||||||
|
|
||||||
const uuid = randomUUID();
|
const uuid = randomUUID();
|
||||||
|
|
||||||
|
const newItems = []; // Array to track newly added items during the sync process
|
||||||
|
|
||||||
syncTask = { loggedData: [], uuid: uuid, wsKey: "PartialSyncTask", taskName: taskName.partialsync };
|
syncTask = { loggedData: [], uuid: uuid, wsKey: "PartialSyncTask", taskName: taskName.partialsync };
|
||||||
try {
|
try {
|
||||||
@@ -832,7 +863,7 @@ async function partialSync(triggertype) {
|
|||||||
if (config.error) {
|
if (config.error) {
|
||||||
syncTask.loggedData.push({ Message: config.error });
|
syncTask.loggedData.push({ Message: config.error });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
return;
|
return { success: false, error: config.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraries = await API.getLibraries();
|
const libraries = await API.getLibraries();
|
||||||
@@ -841,7 +872,7 @@ async function partialSync(triggertype) {
|
|||||||
syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." });
|
syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " " + taskName.fullsync + " Completed" });
|
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 excluded_libraries = config.settings.ExcludedLibraries || [];
|
||||||
@@ -849,10 +880,10 @@ async function partialSync(triggertype) {
|
|||||||
const filtered_libraries = libraries.filter((library) => !excluded_libraries.includes(library.Id));
|
const filtered_libraries = libraries.filter((library) => !excluded_libraries.includes(library.Id));
|
||||||
const existing_excluded_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();
|
await syncUserData();
|
||||||
|
|
||||||
// //syncLibraryFolders
|
// syncLibraryFolders
|
||||||
await syncLibraryFolders(filtered_libraries, existing_excluded_libraries);
|
await syncLibraryFolders(filtered_libraries, existing_excluded_libraries);
|
||||||
|
|
||||||
//item sync counters
|
//item sync counters
|
||||||
@@ -870,7 +901,7 @@ async function partialSync(triggertype) {
|
|||||||
let updateItemInfoCount = 0;
|
let updateItemInfoCount = 0;
|
||||||
let updateEpisodeInfoCount = 0;
|
let updateEpisodeInfoCount = 0;
|
||||||
|
|
||||||
let lastSyncDate = moment().subtract(24, "hours");
|
let lastSyncDate = dayjs().subtract(24, "hours");
|
||||||
|
|
||||||
const last_execution = await db
|
const last_execution = await db
|
||||||
.query(
|
.query(
|
||||||
@@ -881,7 +912,7 @@ async function partialSync(triggertype) {
|
|||||||
)
|
)
|
||||||
.then((res) => res.rows);
|
.then((res) => res.rows);
|
||||||
if (last_execution.length !== 0) {
|
if (last_execution.length !== 0) {
|
||||||
lastSyncDate = moment(last_execution[0].DateCreated);
|
lastSyncDate = dayjs(last_execution[0].DateCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
|
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
|
||||||
@@ -908,7 +939,7 @@ async function partialSync(triggertype) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
libraryItems = libraryItems.filter((item) => moment(item.DateCreated).isAfter(lastSyncDate));
|
libraryItems = libraryItems.filter((item) => dayjs(item.DateCreated).isAfter(lastSyncDate));
|
||||||
|
|
||||||
while (libraryItems.length != 0) {
|
while (libraryItems.length != 0) {
|
||||||
if (libraryItems.length === 0 && startIndex === 0) {
|
if (libraryItems.length === 0 && startIndex === 0) {
|
||||||
@@ -955,7 +986,7 @@ async function partialSync(triggertype) {
|
|||||||
insertEpisodeInfoCount += Number(infoCount.insertEpisodeInfoCount);
|
insertEpisodeInfoCount += Number(infoCount.insertEpisodeInfoCount);
|
||||||
updateEpisodeInfoCount += Number(infoCount.updateEpisodeInfoCount);
|
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;
|
library_items = null;
|
||||||
seasons = null;
|
seasons = null;
|
||||||
episodes = null;
|
episodes = null;
|
||||||
@@ -973,7 +1004,7 @@ async function partialSync(triggertype) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
libraryItems = libraryItems.filter((item) => moment(item.DateCreated).isAfter(lastSyncDate));
|
libraryItems = libraryItems.filter((item) => dayjs(item.DateCreated).isAfter(lastSyncDate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,10 +1053,22 @@ async function partialSync(triggertype) {
|
|||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.SUCCESS);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.SUCCESS);
|
||||||
|
|
||||||
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " Sync Completed" });
|
sendUpdate(syncTask.wsKey, { type: "Success", message: triggertype + " Sync Completed" });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
newItems: newItems,
|
||||||
|
stats: {
|
||||||
|
itemsAdded: insertedItemsCount,
|
||||||
|
episodesAdded: insertedEpisodeCount,
|
||||||
|
seasonsAdded: insertedSeasonsCount
|
||||||
|
}
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
syncTask.loggedData.push({ color: "red", Message: getErrorLineNumber(error) + ": Error: " + error });
|
syncTask.loggedData.push({ color: "red", Message: getErrorLineNumber(error) + ": Error: " + error });
|
||||||
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED);
|
||||||
sendUpdate(syncTask.wsKey, { type: "Error", message: triggertype + " Sync Halted with Errors" });
|
sendUpdate(syncTask.wsKey, { type: "Error", message: triggertype + " Sync Halted with Errors" });
|
||||||
|
|
||||||
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,18 +185,128 @@ router.post('/:id/test', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const webhook = result.rows[0];
|
const webhook = result.rows[0];
|
||||||
const testData = req.body || {};
|
let testData = req.body || {};
|
||||||
|
let success = false;
|
||||||
|
|
||||||
const success = await webhookManager.executeWebhook(webhook, testData);
|
// Discord behaviour
|
||||||
|
if (webhook.url.includes('discord.com/api/webhooks')) {
|
||||||
|
console.log('Discord webhook détecté, préparation du payload spécifique');
|
||||||
|
|
||||||
|
// Discord specific format
|
||||||
|
testData = {
|
||||||
|
content: "Test de webhook depuis Jellystat",
|
||||||
|
embeds: [{
|
||||||
|
title: "Discord test notification",
|
||||||
|
description: "This is a test notification of jellystat discord webhook",
|
||||||
|
color: 3447003,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "Webhook type",
|
||||||
|
value: webhook.trigger_type || "Not specified",
|
||||||
|
inline: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ID",
|
||||||
|
value: webhook.id,
|
||||||
|
inline: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bypass classic method for discord
|
||||||
|
success = await webhookManager.executeDiscordWebhook(webhook, testData);
|
||||||
|
}
|
||||||
|
else if (webhook.trigger_type === 'event' && webhook.event_type) {
|
||||||
|
const eventType = webhook.event_type;
|
||||||
|
|
||||||
|
let eventData = {};
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case 'playback_started':
|
||||||
|
eventData = {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: "test-user-id",
|
||||||
|
deviceId: "test-device-id",
|
||||||
|
deviceName: "Test Device",
|
||||||
|
clientName: "Test Client",
|
||||||
|
isPaused: false,
|
||||||
|
mediaType: "Movie",
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
startTime: new Date().toISOString()
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
username: "Test User",
|
||||||
|
userImageTag: "test-image-tag"
|
||||||
|
},
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: "test-item-id",
|
||||||
|
episodeId: null,
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
seasonName: null,
|
||||||
|
seriesName: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'playback_ended':
|
||||||
|
eventData = {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: "test-user-id",
|
||||||
|
deviceId: "test-device-id",
|
||||||
|
deviceName: "Test Device",
|
||||||
|
clientName: "Test Client",
|
||||||
|
mediaType: "Movie",
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
startTime: new Date(Date.now() - 3600000).toISOString(),
|
||||||
|
endTime: new Date().toISOString(),
|
||||||
|
playbackDuration: 3600
|
||||||
|
},
|
||||||
|
userData: {
|
||||||
|
username: "Test User",
|
||||||
|
userImageTag: "test-image-tag"
|
||||||
|
},
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: "test-item-id",
|
||||||
|
episodeId: null,
|
||||||
|
mediaName: "Test Movie",
|
||||||
|
seasonName: null,
|
||||||
|
seriesName: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'media_recently_added':
|
||||||
|
eventData = {
|
||||||
|
mediaItem: {
|
||||||
|
id: "test-item-id",
|
||||||
|
name: "Test Media",
|
||||||
|
type: "Movie",
|
||||||
|
overview: "This is a test movie for webhook testing",
|
||||||
|
addedDate: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
success = await webhookManager.triggerEventWebhooks(eventType, eventData, [webhook.id]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
success = await webhookManager.executeWebhook(webhook, testData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
success = await webhookManager.executeWebhook(webhook, testData);
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.json({ message: 'Webhook executed successfully' });
|
res.json({ message: 'Webhook executed successfully' });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ error: 'Webhook execution failed' });
|
res.status(500).json({ error: 'Error while executing webhook' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error testing webhook:', error);
|
console.error('Error testing webhook:', error);
|
||||||
res.status(500).json({ error: 'Failed to test webhook' });
|
res.status(500).json({ error: 'Failed to test webhook: ' + error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,10 +315,110 @@ router.post('/:id/trigger-monthly', async (req, res) => {
|
|||||||
const success = await webhookManager.triggerMonthlySummaryWebhook(req.params.id);
|
const success = await webhookManager.triggerMonthlySummaryWebhook(req.params.id);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
res.status(200).json({ message: "Rapport mensuel envoyé avec succès" });
|
res.status(200).json({ message: "Monthly report sent successfully" });
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({ message: "Échec de l'envoi du rapport mensuel" });
|
res.status(500).json({ message: "Failed to send monthly report" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
// Get status of event webhooks
|
||||||
|
router.get('/event-status', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const eventTypes = ['playback_started', 'playback_ended', 'media_recently_added'];
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (const eventType of eventTypes) {
|
||||||
|
const webhooks = await dbInstance.query(
|
||||||
|
'SELECT id, name, enabled FROM webhooks WHERE trigger_type = $1 AND event_type = $2',
|
||||||
|
['event', eventType]
|
||||||
|
);
|
||||||
|
|
||||||
|
result[eventType] = {
|
||||||
|
exists: webhooks.rows.length > 0,
|
||||||
|
enabled: webhooks.rows.some(webhook => webhook.enabled),
|
||||||
|
webhooks: webhooks.rows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching webhook status:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch webhook status' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle all webhooks of a specific event type
|
||||||
|
router.post('/toggle-event/:eventType', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { eventType } = req.params;
|
||||||
|
const { enabled } = req.body;
|
||||||
|
|
||||||
|
if (!['playback_started', 'playback_ended', 'media_recently_added'].includes(eventType)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid event type' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof enabled !== 'boolean') {
|
||||||
|
return res.status(400).json({ error: 'Enabled parameter must be a boolean' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour tous les webhooks de ce type d'événement
|
||||||
|
const result = await dbInstance.query(
|
||||||
|
'UPDATE webhooks SET enabled = $1 WHERE trigger_type = $2 AND event_type = $3 RETURNING id',
|
||||||
|
[enabled, 'event', eventType]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si aucun webhook n'existe pour ce type, en créer un de base
|
||||||
|
if (result.rows.length === 0 && enabled) {
|
||||||
|
const defaultWebhook = {
|
||||||
|
name: `Webhook pour ${eventType}`,
|
||||||
|
url: req.body.url || '',
|
||||||
|
method: 'POST',
|
||||||
|
trigger_type: 'event',
|
||||||
|
event_type: eventType,
|
||||||
|
enabled: true,
|
||||||
|
headers: '{}',
|
||||||
|
payload: JSON.stringify({
|
||||||
|
event: `{{event}}`,
|
||||||
|
data: `{{data}}`,
|
||||||
|
timestamp: `{{triggeredAt}}`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!defaultWebhook.url) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'URL parameter is required when creating a new webhook',
|
||||||
|
needsUrl: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbInstance.query(
|
||||||
|
`INSERT INTO webhooks (name, url, method, trigger_type, event_type, enabled, headers, payload)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||||
|
[
|
||||||
|
defaultWebhook.name,
|
||||||
|
defaultWebhook.url,
|
||||||
|
defaultWebhook.method,
|
||||||
|
defaultWebhook.trigger_type,
|
||||||
|
defaultWebhook.event_type,
|
||||||
|
defaultWebhook.enabled,
|
||||||
|
defaultWebhook.headers,
|
||||||
|
defaultWebhook.payload
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rafraîchir le planificateur de webhooks
|
||||||
|
await webhookScheduler.refreshSchedule();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Webhooks for ${eventType} ${enabled ? 'enabled' : 'disabled'}`,
|
||||||
|
affectedCount: result.rows.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling webhooks:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to toggle webhooks' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|||||||
@@ -3504,7 +3504,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/stats/getViewsOverTime": {
|
"/stats/getViewsOverTime": {
|
||||||
"post": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Stats"
|
"Stats"
|
||||||
],
|
],
|
||||||
@@ -3526,16 +3526,9 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "body",
|
"name": "days",
|
||||||
"in": "body",
|
"in": "query",
|
||||||
"schema": {
|
"type": "string"
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"days": {
|
|
||||||
"example": "any"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -3558,7 +3551,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/stats/getViewsByDays": {
|
"/stats/getViewsByDays": {
|
||||||
"post": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Stats"
|
"Stats"
|
||||||
],
|
],
|
||||||
@@ -3580,16 +3573,9 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "body",
|
"name": "days",
|
||||||
"in": "body",
|
"in": "query",
|
||||||
"schema": {
|
"type": "string"
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"days": {
|
|
||||||
"example": "any"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -3612,7 +3598,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/stats/getViewsByHour": {
|
"/stats/getViewsByHour": {
|
||||||
"post": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Stats"
|
"Stats"
|
||||||
],
|
],
|
||||||
@@ -3634,16 +3620,56 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "body",
|
"name": "days",
|
||||||
"in": "body",
|
"in": "query",
|
||||||
"schema": {
|
"type": "string"
|
||||||
"type": "object",
|
}
|
||||||
"properties": {
|
],
|
||||||
"days": {
|
"responses": {
|
||||||
"example": "any"
|
"200": {
|
||||||
}
|
"description": "OK"
|
||||||
}
|
},
|
||||||
}
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
},
|
||||||
|
"503": {
|
||||||
|
"description": "Service Unavailable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/stats/getViewsByLibraryType": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Stats"
|
||||||
|
],
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "authorization",
|
||||||
|
"in": "header",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "x-api-token",
|
||||||
|
"in": "header",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "req",
|
||||||
|
"in": "query",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "days",
|
||||||
|
"in": "query",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
const db = require("../db");
|
const db = require("../db");
|
||||||
|
|
||||||
const moment = require("moment");
|
const dayjs = require("dayjs");
|
||||||
const { columnsPlayback } = require("../models/jf_playback_activity");
|
const { columnsPlayback } = require("../models/jf_playback_activity");
|
||||||
const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require("../models/jf_activity_watchdog");
|
const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require("../models/jf_activity_watchdog");
|
||||||
const configClass = require("../classes/config");
|
const configClass = require("../classes/config");
|
||||||
const API = require("../classes/api-loader");
|
const API = require("../classes/api-loader");
|
||||||
const { sendUpdate } = require("../ws");
|
const { sendUpdate } = require("../ws");
|
||||||
const { isNumber } = require("@mui/x-data-grid/internals");
|
const { isNumber } = require("@mui/x-data-grid/internals");
|
||||||
|
const WebhookManager = require("../classes/webhook-manager");
|
||||||
|
|
||||||
const MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK = process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK
|
const MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK = process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK
|
||||||
? Number(process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK)
|
? Number(process.env.MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK)
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
const webhookManager = new WebhookManager();
|
||||||
let existingData = await WatchdogData.filter((wdData) => {
|
|
||||||
return SessionData.some((sessionData) => {
|
|
||||||
let NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
|
||||||
|
|
||||||
let matchesEpisodeId =
|
async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
||||||
|
const existingData = await WatchdogData.filter((wdData) => {
|
||||||
|
return SessionData.some((sessionData) => {
|
||||||
|
const NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||||
|
|
||||||
|
const matchesEpisodeId =
|
||||||
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
||||||
|
|
||||||
let matchingSessionFound =
|
const matchingSessionFound =
|
||||||
// wdData.Id === sessionData.Id &&
|
// wdData.Id === sessionData.Id &&
|
||||||
wdData.UserId === sessionData.UserId &&
|
wdData.UserId === sessionData.UserId &&
|
||||||
wdData.DeviceId === sessionData.DeviceId &&
|
wdData.DeviceId === sessionData.DeviceId &&
|
||||||
@@ -31,16 +35,16 @@ async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
|||||||
|
|
||||||
//if the playstate was paused, calculate the difference in seconds and add to the playback duration
|
//if the playstate was paused, calculate the difference in seconds and add to the playback duration
|
||||||
if (sessionData.PlayState.IsPaused == true) {
|
if (sessionData.PlayState.IsPaused == true) {
|
||||||
let startTime = moment(wdData.ActivityDateInserted, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
const startTime = dayjs(wdData.ActivityDateInserted);
|
||||||
let lastPausedDate = moment(sessionData.LastPausedDate);
|
const lastPausedDate = dayjs(sessionData.LastPausedDate, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
|
|
||||||
let diffInSeconds = lastPausedDate.diff(startTime, "seconds");
|
const diffInSeconds = lastPausedDate.diff(startTime, "seconds");
|
||||||
|
|
||||||
wdData.PlaybackDuration = parseInt(wdData.PlaybackDuration) + diffInSeconds;
|
wdData.PlaybackDuration = parseInt(wdData.PlaybackDuration) + diffInSeconds;
|
||||||
|
|
||||||
wdData.ActivityDateInserted = `${lastPausedDate.format("YYYY-MM-DD HH:mm:ss.SSSZ")}`;
|
wdData.ActivityDateInserted = `${lastPausedDate.format("YYYY-MM-DD HH:mm:ss.SSSZ")}`;
|
||||||
} else {
|
} else {
|
||||||
wdData.ActivityDateInserted = moment().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
wdData.ActivityDateInserted = dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -52,15 +56,15 @@ async function getSessionsInWatchDog(SessionData, WatchdogData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getSessionsNotInWatchDog(SessionData, WatchdogData) {
|
async function getSessionsNotInWatchDog(SessionData, WatchdogData) {
|
||||||
let newData = await SessionData.filter((sessionData) => {
|
const newData = await SessionData.filter((sessionData) => {
|
||||||
if (WatchdogData.length === 0) return true;
|
if (WatchdogData.length === 0) return true;
|
||||||
return !WatchdogData.some((wdData) => {
|
return !WatchdogData.some((wdData) => {
|
||||||
let NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
const NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||||
|
|
||||||
let matchesEpisodeId =
|
const matchesEpisodeId =
|
||||||
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
||||||
|
|
||||||
let matchingSessionFound =
|
const matchingSessionFound =
|
||||||
// wdData.Id === sessionData.Id &&
|
// wdData.Id === sessionData.Id &&
|
||||||
wdData.UserId === sessionData.UserId &&
|
wdData.UserId === sessionData.UserId &&
|
||||||
wdData.DeviceId === sessionData.DeviceId &&
|
wdData.DeviceId === sessionData.DeviceId &&
|
||||||
@@ -75,15 +79,15 @@ async function getSessionsNotInWatchDog(SessionData, WatchdogData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
||||||
let removedData = WatchdogData.filter((wdData) => {
|
const removedData = WatchdogData.filter((wdData) => {
|
||||||
if (SessionData.length === 0) return true;
|
if (SessionData.length === 0) return true;
|
||||||
return !SessionData.some((sessionData) => {
|
return !SessionData.some((sessionData) => {
|
||||||
let NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
const NowPlayingItemId = sessionData.NowPlayingItem.SeriesId || sessionData.NowPlayingItem.Id;
|
||||||
|
|
||||||
let matchesEpisodeId =
|
const matchesEpisodeId =
|
||||||
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
sessionData.NowPlayingItem.SeriesId != undefined ? wdData.EpisodeId === sessionData.NowPlayingItem.Id : true;
|
||||||
|
|
||||||
let noMatchingSessionFound =
|
const noMatchingSessionFound =
|
||||||
// wdData.Id === sessionData.Id &&
|
// wdData.Id === sessionData.Id &&
|
||||||
wdData.UserId === sessionData.UserId &&
|
wdData.UserId === sessionData.UserId &&
|
||||||
wdData.DeviceId === sessionData.DeviceId &&
|
wdData.DeviceId === sessionData.DeviceId &&
|
||||||
@@ -97,10 +101,10 @@ function getWatchDogNotInSessions(SessionData, WatchdogData) {
|
|||||||
|
|
||||||
removedData.map((obj) => {
|
removedData.map((obj) => {
|
||||||
obj.Id = obj.ActivityId;
|
obj.Id = obj.ActivityId;
|
||||||
let startTime = moment(obj.ActivityDateInserted, "YYYY-MM-DD HH:mm:ss.SSSZ");
|
const startTime = dayjs(obj.ActivityDateInserted);
|
||||||
let endTime = moment();
|
const endTime = dayjs();
|
||||||
|
|
||||||
let diffInSeconds = endTime.diff(startTime, "seconds");
|
const diffInSeconds = endTime.diff(startTime, "seconds");
|
||||||
|
|
||||||
if (obj.IsPaused == false) {
|
if (obj.IsPaused == false) {
|
||||||
obj.PlaybackDuration = parseInt(obj.PlaybackDuration) + diffInSeconds;
|
obj.PlaybackDuration = parseInt(obj.PlaybackDuration) + diffInSeconds;
|
||||||
@@ -114,20 +118,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);
|
||||||
|
|
||||||
@@ -137,15 +191,51 @@ async function ActivityMonitor(interval) {
|
|||||||
}
|
}
|
||||||
// New Code
|
// New Code
|
||||||
|
|
||||||
let WatchdogDataToInsert = await getSessionsNotInWatchDog(SessionData, WatchdogData);
|
const WatchdogDataToInsert = await getSessionsNotInWatchDog(SessionData, WatchdogData);
|
||||||
let WatchdogDataToUpdate = await getSessionsInWatchDog(SessionData, WatchdogData);
|
const WatchdogDataToUpdate = await getSessionsInWatchDog(SessionData, WatchdogData);
|
||||||
let dataToRemove = await getWatchDogNotInSessions(SessionData, WatchdogData);
|
const dataToRemove = await getWatchDogNotInSessions(SessionData, WatchdogData);
|
||||||
|
|
||||||
/////////////////
|
/////////////////
|
||||||
|
|
||||||
//filter fix if table is empty
|
//filter fix if table is empty
|
||||||
|
|
||||||
if (WatchdogDataToInsert.length > 0) {
|
if (WatchdogDataToInsert.length > 0) {
|
||||||
|
for (const session of WatchdogDataToInsert) {
|
||||||
|
let userData = {};
|
||||||
|
try {
|
||||||
|
const userInfo = await API.getUserById(session.UserId);
|
||||||
|
if (userInfo) {
|
||||||
|
userData = {
|
||||||
|
username: userInfo.Name,
|
||||||
|
userImageTag: userInfo.PrimaryImageTag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error fetching user data: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webhookManager.triggerEventWebhooks('playback_started', {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: session.UserId,
|
||||||
|
deviceId: session.DeviceId,
|
||||||
|
deviceName: session.DeviceName,
|
||||||
|
clientName: session.ClientName,
|
||||||
|
isPaused: session.IsPaused,
|
||||||
|
mediaType: session.MediaType,
|
||||||
|
mediaName: session.NowPlayingItemName,
|
||||||
|
startTime: session.ActivityDateInserted
|
||||||
|
},
|
||||||
|
userData,
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: session.NowPlayingItemId,
|
||||||
|
episodeId: session.EpisodeId,
|
||||||
|
mediaName: session.NowPlayingItemName,
|
||||||
|
seasonName: session.SeasonName,
|
||||||
|
seriesName: session.SeriesName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//insert new rows where not existing items
|
//insert new rows where not existing items
|
||||||
// console.log("Inserted " + WatchdogDataToInsert.length + " wd playback records");
|
// console.log("Inserted " + WatchdogDataToInsert.length + " wd playback records");
|
||||||
db.insertBulk("jf_activity_watchdog", WatchdogDataToInsert, jf_activity_watchdog_columns);
|
db.insertBulk("jf_activity_watchdog", WatchdogDataToInsert, jf_activity_watchdog_columns);
|
||||||
@@ -158,11 +248,46 @@ async function ActivityMonitor(interval) {
|
|||||||
console.log("Existing Data Updated: ", WatchdogDataToUpdate.length);
|
console.log("Existing Data Updated: ", WatchdogDataToUpdate.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataToRemove.length > 0) {
|
||||||
|
for (const session of dataToRemove) {
|
||||||
|
let userData = {};
|
||||||
|
try {
|
||||||
|
const userInfo = await API.getUserById(session.UserId);
|
||||||
|
if (userInfo) {
|
||||||
|
userData = {
|
||||||
|
username: userInfo.Name,
|
||||||
|
userImageTag: userInfo.PrimaryImageTag
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[WEBHOOK] Error fetching user data: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await webhookManager.triggerEventWebhooks('playback_ended', {
|
||||||
|
sessionInfo: {
|
||||||
|
userId: session.UserId,
|
||||||
|
deviceId: session.DeviceId,
|
||||||
|
deviceName: session.DeviceName,
|
||||||
|
clientName: session.ClientName,
|
||||||
|
playbackDuration: session.PlaybackDuration,
|
||||||
|
endTime: session.ActivityDateInserted
|
||||||
|
},
|
||||||
|
userData,
|
||||||
|
mediaInfo: {
|
||||||
|
itemId: session.NowPlayingItemId,
|
||||||
|
episodeId: session.EpisodeId,
|
||||||
|
mediaName: session.NowPlayingItemName,
|
||||||
|
seasonName: session.SeasonName,
|
||||||
|
seriesName: session.SeriesName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const toDeleteIds = dataToRemove.map((row) => row.ActivityId);
|
||||||
|
|
||||||
//delete from db no longer in session data and insert into stats db
|
//delete from db no longer in session data and insert into stats db
|
||||||
//Bulk delete from db thats no longer on api
|
//Bulk delete from db thats no longer on api
|
||||||
|
|
||||||
const toDeleteIds = dataToRemove.map((row) => row.ActivityId);
|
|
||||||
|
|
||||||
let playbackToInsert = dataToRemove;
|
let playbackToInsert = dataToRemove;
|
||||||
|
|
||||||
if (playbackToInsert.length == 0 && toDeleteIds.length == 0) {
|
if (playbackToInsert.length == 0 && toDeleteIds.length == 0) {
|
||||||
@@ -172,7 +297,7 @@ async function ActivityMonitor(interval) {
|
|||||||
/////get data from jf_playback_activity within the last hour with progress of <=80% for current items in session
|
/////get data from jf_playback_activity within the last hour with progress of <=80% for current items in session
|
||||||
|
|
||||||
const ExistingRecords = await db
|
const ExistingRecords = await db
|
||||||
.query(`SELECT * FROM jf_recent_playback_activity(1) limit 0`)
|
.query(`SELECT * FROM jf_recent_playback_activity(1)`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.rows && Array.isArray(res.rows) && res.rows.length > 0) {
|
if (res.rows && Array.isArray(res.rows) && res.rows.length > 0) {
|
||||||
return res.rows.filter(
|
return res.rows.filter(
|
||||||
@@ -212,7 +337,7 @@ async function ActivityMonitor(interval) {
|
|||||||
if (existingrow) {
|
if (existingrow) {
|
||||||
playbackData.Id = existingrow.Id;
|
playbackData.Id = existingrow.Id;
|
||||||
playbackData.PlaybackDuration = Number(existingrow.PlaybackDuration) + Number(playbackData.PlaybackDuration);
|
playbackData.PlaybackDuration = Number(existingrow.PlaybackDuration) + Number(playbackData.PlaybackDuration);
|
||||||
playbackData.ActivityDateInserted = moment().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
playbackData.ActivityDateInserted = dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -248,7 +373,9 @@ async function ActivityMonitor(interval) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
} catch (error) {
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
if (error?.code === "ECONNREFUSED") {
|
if (error?.code === "ECONNREFUSED") {
|
||||||
console.error("Error: Unable to connect to API"); //TO-DO Change this to correct API name
|
console.error("Error: Unable to connect to API"); //TO-DO Change this to correct API name
|
||||||
} else if (error?.code === "ERR_BAD_RESPONSE") {
|
} else if (error?.code === "ERR_BAD_RESPONSE") {
|
||||||
@@ -258,7 +385,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 = {
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ async function runBackupTask(triggerType = triggertype.Automatic) {
|
|||||||
|
|
||||||
console.log("Running Scheduled Backup");
|
console.log("Running Scheduled Backup");
|
||||||
|
|
||||||
Logging.insertLog(uuid, triggerType, taskName.backup);
|
await Logging.insertLog(uuid, triggerType, taskName.backup);
|
||||||
|
|
||||||
await backup(refLog);
|
await backup(refLog);
|
||||||
Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
|
await Logging.updateLog(uuid, refLog.logData, taskstate.SUCCESS);
|
||||||
sendUpdate("BackupTask", { type: "Success", message: `${triggerType} Backup Completed` });
|
sendUpdate("BackupTask", { type: "Success", message: `${triggerType} Backup Completed` });
|
||||||
console.log("Scheduled Backup Complete");
|
console.log("Scheduled Backup Complete");
|
||||||
parentPort.postMessage({ status: "complete" });
|
parentPort.postMessage({ status: "complete" });
|
||||||
@@ -42,8 +42,9 @@ async function runBackupTask(triggerType = triggertype.Automatic) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parentPort.on("message", (message) => {
|
parentPort.on("message", async (message) => {
|
||||||
if (message.command === "start") {
|
if (message.command === "start") {
|
||||||
runBackupTask(message.triggertype);
|
await runBackupTask(message.triggertype);
|
||||||
|
process.exit(0); // Exit the worker after the task is done
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ async function runFullSyncTask(triggerType = triggertype.Automatic) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parentPort.on("message", (message) => {
|
parentPort.on("message", async (message) => {
|
||||||
if (message.command === "start") {
|
if (message.command === "start") {
|
||||||
runFullSyncTask(message.triggertype);
|
await runFullSyncTask(message.triggertype);
|
||||||
|
process.exit(0); // Exit the worker after the task is done
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ async function runPlaybackReportingPluginSyncTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parentPort.on("message", (message) => {
|
parentPort.on("message", async (message) => {
|
||||||
if (message.command === "start") {
|
if (message.command === "start") {
|
||||||
runPlaybackReportingPluginSyncTask();
|
await runPlaybackReportingPluginSyncTask();
|
||||||
|
process.exit(0); // Exit the worker after the task is done
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { parentPort } = require("worker_threads");
|
const { parentPort } = require("worker_threads");
|
||||||
const triggertype = require("../logging/triggertype");
|
const triggertype = require("../logging/triggertype");
|
||||||
const sync = require("../routes/sync");
|
const sync = require("../routes/sync");
|
||||||
|
const WebhookManager = require("../classes/webhook-manager");
|
||||||
|
|
||||||
async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
||||||
try {
|
try {
|
||||||
@@ -17,19 +18,33 @@ async function runPartialSyncTask(triggerType = triggertype.Automatic) {
|
|||||||
});
|
});
|
||||||
parentPort.postMessage({ type: "log", message: formattedArgs.join(" ") });
|
parentPort.postMessage({ type: "log", message: formattedArgs.join(" ") });
|
||||||
};
|
};
|
||||||
await sync.partialSync(triggerType);
|
|
||||||
|
const syncResults = await sync.partialSync(triggerType);
|
||||||
|
|
||||||
|
const webhookManager = new WebhookManager();
|
||||||
|
|
||||||
|
const newMediaCount = syncResults?.newItems?.length || 0;
|
||||||
|
|
||||||
|
if (newMediaCount > 0) {
|
||||||
|
await webhookManager.triggerEventWebhooks('media_recently_added', {
|
||||||
|
count: newMediaCount,
|
||||||
|
items: syncResults.newItems,
|
||||||
|
syncDate: new Date().toISOString(),
|
||||||
|
triggerType: triggerType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
parentPort.postMessage({ status: "complete" });
|
parentPort.postMessage({ status: "complete" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
parentPort.postMessage({ status: "error", message: error.message });
|
parentPort.postMessage({ status: "error", message: error.message });
|
||||||
|
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parentPort.on("message", (message) => {
|
parentPort.on("message", async (message) => {
|
||||||
if (message.command === "start") {
|
if (message.command === "start") {
|
||||||
runPartialSyncTask(message.triggertype);
|
await runPartialSyncTask(message.triggertype);
|
||||||
|
process.exit(0); // Exit the worker after the task is done
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Jellyfin stats for the masses" />
|
<meta name="description" content="Jellyfin stats for the masses" />
|
||||||
<link rel="apple-touch-icon" href="icon-b-192.png" />
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||||
<script src="env.js"></script>
|
<script src="env.js"></script>
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jfstat",
|
"name": "jfstat",
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jfstat",
|
"name": "jfstat",
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"config": "^3.3.9",
|
"config": "^3.3.9",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"dns-cache": "^2.0.0",
|
"dns-cache": "^2.0.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"dottie": "^2.0.6",
|
"dottie": "^2.0.6",
|
||||||
@@ -44,7 +45,6 @@
|
|||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"material-react-table": "^3.1.0",
|
"material-react-table": "^3.1.0",
|
||||||
"memoizee": "^0.4.17",
|
"memoizee": "^0.4.17",
|
||||||
"moment": "^2.29.4",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
@@ -8799,9 +8799,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.10",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jfstat",
|
"name": "jfstat",
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/index.jsx",
|
"main": "src/index.jsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"config": "^3.3.9",
|
"config": "^3.3.9",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"dns-cache": "^2.0.0",
|
"dns-cache": "^2.0.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"dottie": "^2.0.6",
|
"dottie": "^2.0.6",
|
||||||
@@ -51,7 +52,6 @@
|
|||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"material-react-table": "^3.1.0",
|
"material-react-table": "^3.1.0",
|
||||||
"memoizee": "^0.4.17",
|
"memoizee": "^0.4.17",
|
||||||
"moment": "^2.29.4",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
|
|||||||
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
319
public/locales/de-DE/translation.json
Normal file
319
public/locales/de-DE/translation.json
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
{
|
||||||
|
"JELLYSTAT": "Jellystat",
|
||||||
|
"MENU_TABS": {
|
||||||
|
"HOME": "Startseite",
|
||||||
|
"LIBRARIES": "Bibliotheken",
|
||||||
|
"USERS": "Benutzer",
|
||||||
|
"ACTIVITY": "Aktivitäten",
|
||||||
|
"STATISTICS": "Statistiken",
|
||||||
|
"SETTINGS": "Einstellungen",
|
||||||
|
"ABOUT": "Über",
|
||||||
|
"LOGOUT": "Abmelden",
|
||||||
|
"TIMELINE": "Zeitleiste"
|
||||||
|
},
|
||||||
|
"HOME_PAGE": {
|
||||||
|
"SESSIONS": "Sitzungen",
|
||||||
|
"RECENTLY_ADDED": "Zuletzt hinzugefügt",
|
||||||
|
"WATCH_STATISTIC": "Wiedergabestatistiken",
|
||||||
|
"LIBRARY_OVERVIEW": "Bibliothek-Übersicht"
|
||||||
|
},
|
||||||
|
"SESSIONS": {
|
||||||
|
"NO_SESSIONS": "Keine aktiven Sitzungen gefunden",
|
||||||
|
"DIRECT_PLAY": "Direkte Wiedergabe",
|
||||||
|
"TRANSCODE": "Transkodieren"
|
||||||
|
},
|
||||||
|
"STAT_CARDS": {
|
||||||
|
"MOST_VIEWED_MOVIES": "MEISTGESEHENE FILME",
|
||||||
|
"MOST_POPULAR_MOVIES": "BELIEBTESTE FILME",
|
||||||
|
"MOST_VIEWED_SERIES": "MEISTGESEHENE SERIEN",
|
||||||
|
"MOST_POPULAR_SERIES": "BELIEBTESTE SERIEN",
|
||||||
|
"MOST_LISTENED_MUSIC": "MEISTGEHÖRTE MUSIK",
|
||||||
|
"MOST_POPULAR_MUSIC": "BELIEBTESTE MUSIK",
|
||||||
|
"MOST_VIEWED_LIBRARIES": "MEISTGESEHENE BIBLIOTHEKEN",
|
||||||
|
"MOST_USED_CLIENTS": "MEISTGENUTZTE CLIENTS",
|
||||||
|
"MOST_ACTIVE_USERS": "AKTIVSTE BENUTZER",
|
||||||
|
"CONCURRENT_STREAMS": "GLEICHZEITIGE STREAMS"
|
||||||
|
},
|
||||||
|
"LIBRARY_OVERVIEW": {
|
||||||
|
"MOVIE_LIBRARIES": "FILM-BIBLIOTHEKEN",
|
||||||
|
"SHOW_LIBRARIES": "SERIEN-BIBLIOTHEKEN",
|
||||||
|
"MUSIC_LIBRARIES": "MUSIK-BIBLIOTHEKEN",
|
||||||
|
"MIXED_LIBRARIES": "GEMISCHTE BIBLIOTHEKEN"
|
||||||
|
},
|
||||||
|
"LIBRARY_CARD": {
|
||||||
|
"LIBRARY": "Bibliothek",
|
||||||
|
"TOTAL_TIME": "Gesamtlaufzeit",
|
||||||
|
"TOTAL_FILES": "Gesamtzahl der Dateien",
|
||||||
|
"LIBRARY_SIZE": "Größe der Bibliothek",
|
||||||
|
"TOTAL_PLAYBACK": "Gesamtwiedergabezeit",
|
||||||
|
"LAST_PLAYED": "Zuletzt gespielt",
|
||||||
|
"LAST_ACTIVITY": "Letzte Aktivität",
|
||||||
|
"TRACKED": "Daten-Tracking"
|
||||||
|
},
|
||||||
|
"GLOBAL_STATS": {
|
||||||
|
"LAST_24_HRS": "Letzten 24 Stunden",
|
||||||
|
"LAST_7_DAYS": "Letzten 7 Tage",
|
||||||
|
"LAST_30_DAYS": "Letzten 30 Tage",
|
||||||
|
"LAST_180_DAYS": "Letzten 180 Tage",
|
||||||
|
"LAST_365_DAYS": "Letzten 365 Tage",
|
||||||
|
"ALL_TIME": "Gesamtzeit",
|
||||||
|
"ITEM_STATS": "Statistik"
|
||||||
|
},
|
||||||
|
"ITEM_INFO": {
|
||||||
|
"FILE_PATH": "Dateipfad",
|
||||||
|
"FILE_SIZE": "Dateigröße",
|
||||||
|
"RUNTIME": "Laufzeit",
|
||||||
|
"AVERAGE_RUNTIME": "Durchschnittliche Laufzeit",
|
||||||
|
"OPEN_IN_JELLYFIN": "In Jellyfin öffnen",
|
||||||
|
"ARCHIVED_DATA_OPTIONS": "Optionen für archivierte Daten",
|
||||||
|
"PURGE": "Löschen",
|
||||||
|
"CONFIRM_ACTION": "Aktion bestätigen",
|
||||||
|
"CONFIRM_ACTION_MESSAGE": "Sind Sie sicher, dass Sie dieses Element löschen möchten",
|
||||||
|
"CONFIRM_ACTION_MESSAGE_2": "und zugehörige Wiedergabeaktivitäten"
|
||||||
|
},
|
||||||
|
"LIBRARY_INFO": {
|
||||||
|
"LIBRARY_STATS": "Bibliothek-Statistiken",
|
||||||
|
"LIBRARY_ACTIVITY": "Bibliothek-Aktivität"
|
||||||
|
},
|
||||||
|
"TAB_CONTROLS": {
|
||||||
|
"OVERVIEW": "Übersicht",
|
||||||
|
"ACTIVITY": "Aktivität",
|
||||||
|
"OPTIONS": "Optionen",
|
||||||
|
"TIMELINE": "Zeitleiste"
|
||||||
|
},
|
||||||
|
"ITEM_ACTIVITY": "Elementaktivität",
|
||||||
|
"ACTIVITY_TABLE": {
|
||||||
|
"MODAL": {
|
||||||
|
"HEADER": "Stream-Informationen"
|
||||||
|
},
|
||||||
|
"IP_ADDRESS": "IP-Adresse",
|
||||||
|
"CLIENT": "Client",
|
||||||
|
"DEVICE": "Gerät",
|
||||||
|
"PLAYBACK_DURATION": "Wiedergabedauer",
|
||||||
|
"TOTAL_PLAYBACK": "Gesamtwiedergabezeit",
|
||||||
|
"EXPAND": "Erweitern",
|
||||||
|
"COLLAPSE": "Reduzieren",
|
||||||
|
"SORT_BY": "Sortieren nach",
|
||||||
|
"ASCENDING": "Aufsteigend",
|
||||||
|
"DESCENDING": "Absteigend",
|
||||||
|
"CLEAR_SORT": "Sortierung aufheben",
|
||||||
|
"CLEAR_FILTER": "Filter löschen",
|
||||||
|
"FILTER_BY": "Filtern nach",
|
||||||
|
"COLUMN_ACTIONS": "Spaltenaktionen",
|
||||||
|
"TOGGLE_SELECT_ROW": "Zeile auswählen/abwählen",
|
||||||
|
"TOGGLE_SELECT_ALL": "Alle auswählen/abwählen",
|
||||||
|
"MIN": "Min",
|
||||||
|
"MAX": "Max"
|
||||||
|
},
|
||||||
|
"TABLE_NAV_BUTTONS": {
|
||||||
|
"FIRST": "Erste",
|
||||||
|
"LAST": "Letzte",
|
||||||
|
"NEXT": "Nächste",
|
||||||
|
"PREVIOUS": "Vorherige"
|
||||||
|
},
|
||||||
|
"PURGE_OPTIONS": {
|
||||||
|
"PURGE_CACHE": "Zwischengespeichertes Element löschen",
|
||||||
|
"PURGE_CACHE_WITH_ACTIVITY": "Zwischengespeichertes Element und Wiedergabeaktivität löschen",
|
||||||
|
"PURGE_LIBRARY_CACHE": "Zwischengespeicherte Bibliothek und Elemente löschen",
|
||||||
|
"PURGE_LIBRARY_CACHE_WITH_ACTIVITY": "Zwischengespeicherte Bibliothek, Elemente und Aktivität löschen",
|
||||||
|
"PURGE_LIBRARY_ITEMS_CACHE": "Nur zwischengespeicherte Bibliothekelemente löschen",
|
||||||
|
"PURGE_LIBRARY_ITEMS_CACHE_WITH_ACTIVITY": "Nur zwischengespeicherte Bibliothekelemente und Aktivität löschen",
|
||||||
|
"PURGE_ACTIVITY": "Möchten Sie die ausgewählte Wiedergabeaktivität wirklich löschen?"
|
||||||
|
},
|
||||||
|
"ERROR_MESSAGES": {
|
||||||
|
"FETCH_THIS_ITEM": "Dieses Element von Jellyfin abrufen",
|
||||||
|
"NO_ACTIVITY": "Keine Aktivität gefunden",
|
||||||
|
"NEVER": "Nie",
|
||||||
|
"N/A": "N/A",
|
||||||
|
"NO_STATS": "Keine Statistiken zum Anzeigen",
|
||||||
|
"NO_BACKUPS": "Keine Sicherungen gefunden",
|
||||||
|
"NO_LOGS": "Keine Protokolle gefunden",
|
||||||
|
"NO_API_KEYS": "Keine Schlüssel gefunden",
|
||||||
|
"NETWORK_ERROR": "Verbindung zum Jellyfin-Server nicht möglich",
|
||||||
|
"INVALID_LOGIN": "Ungültiger Benutzername oder Passwort",
|
||||||
|
"INVALID_URL": "Fehler {STATUS}: Die angeforderte URL wurde nicht gefunden.",
|
||||||
|
"UNAUTHORIZED": "Fehler {STATUS}: Nicht autorisiert",
|
||||||
|
"PASSWORD_LENGTH": "Passwort muss mindestens 6 Zeichen lang sein",
|
||||||
|
"USERNAME_REQUIRED": "Benutzername ist erforderlich"
|
||||||
|
},
|
||||||
|
"SHOW_ARCHIVED_LIBRARIES": "Archivierte Bibliotheken anzeigen",
|
||||||
|
"HIDE_ARCHIVED_LIBRARIES": "Archivierte Bibliotheken ausblenden",
|
||||||
|
"UNITS": {
|
||||||
|
"YEAR": "Jahr",
|
||||||
|
"YEARS": "Jahre",
|
||||||
|
"MONTH": "Monat",
|
||||||
|
"MONTHS": "Monate",
|
||||||
|
"DAY": "Tag",
|
||||||
|
"DAYS": "Tage",
|
||||||
|
"HOUR": "Stunde",
|
||||||
|
"HOURS": "Stunden",
|
||||||
|
"MINUTE": "Minute",
|
||||||
|
"MINUTES": "Minuten",
|
||||||
|
"SECOND": "Sekunde",
|
||||||
|
"SECONDS": "Sekunden",
|
||||||
|
"PLAYS": "Wiedergaben",
|
||||||
|
"ITEMS": "Elemente",
|
||||||
|
"STREAMS": "Streams"
|
||||||
|
},
|
||||||
|
"USERS_PAGE": {
|
||||||
|
"ALL_USERS": "Alle Benutzer",
|
||||||
|
"LAST_CLIENT": "Letzter Client",
|
||||||
|
"LAST_SEEN": "Zuletzt gesehen",
|
||||||
|
"AGO": "vor",
|
||||||
|
"AGO_ALT": "",
|
||||||
|
"USER_STATS": "Benutzerstatistiken",
|
||||||
|
"USER_ACTIVITY": "Benutzeraktivität"
|
||||||
|
},
|
||||||
|
"STAT_PAGE": {
|
||||||
|
"STATISTICS": "Statistiken",
|
||||||
|
"DAILY_PLAY_PER_LIBRARY": "Tägliche Wiedergabezahl pro Bibliothek",
|
||||||
|
"PLAY_COUNT_BY": "Wiedergabezahl nach"
|
||||||
|
},
|
||||||
|
"SETTINGS_PAGE": {
|
||||||
|
"SETTINGS": "Allgemein",
|
||||||
|
"LANGUAGE": "Sprache",
|
||||||
|
"SELECT_AN_ADMIN": "Einen bevorzugten Administrator auswählen",
|
||||||
|
"LIBRARY_SETTINGS": "Bibliothek",
|
||||||
|
"BACKUP": "Sicherung",
|
||||||
|
"BACKUPS": "Sicherungen",
|
||||||
|
"CHOOSE_FILE": "Datei auswählen",
|
||||||
|
"LOGS": "Protokolle",
|
||||||
|
"SIZE": "Größe",
|
||||||
|
"JELLYFIN_URL": "Jellyfin URL",
|
||||||
|
"EMBY_URL": "Emby URL",
|
||||||
|
"EXTERNAL_URL": "Externe URL",
|
||||||
|
"API_KEY": "API-Schlüssel",
|
||||||
|
"API_KEYS": "API-Schlüssel",
|
||||||
|
"KEY_NAME": "Schlüsselname",
|
||||||
|
"KEY": "Schlüssel",
|
||||||
|
"NAME": "Name",
|
||||||
|
"ADD_KEY": "Schlüssel hinzufügen",
|
||||||
|
"DURATION": "Dauer",
|
||||||
|
"EXECUTION_TYPE": "Ausführungstyp",
|
||||||
|
"RESULTS": "Ergebnisse",
|
||||||
|
"SELECT_ADMIN": "Bevorzugtes Administratorkonto auswählen",
|
||||||
|
"HOUR_FORMAT": "Stundenformat",
|
||||||
|
"HOUR_FORMAT_12": "12 Stunden",
|
||||||
|
"HOUR_FORMAT_24": "24 Stunden",
|
||||||
|
"SECURITY": "Sicherheit",
|
||||||
|
"CURRENT_PASSWORD": "Aktuelles Passwort",
|
||||||
|
"NEW_PASSWORD": "Neues Passwort",
|
||||||
|
"UPDATE": "Aktualisieren",
|
||||||
|
"REQUIRE_LOGIN": "Anmeldung erforderlich",
|
||||||
|
"TASK": "Aufgabe",
|
||||||
|
"TASKS": "Aufgaben",
|
||||||
|
"INTERVAL": "Intervall",
|
||||||
|
"INTERVALS": {
|
||||||
|
"15_MIN": "15 Minuten",
|
||||||
|
"30_MIN": "30 Minuten",
|
||||||
|
"1_HOUR": "1 Stunde",
|
||||||
|
"12_HOURS": "12 Stunden",
|
||||||
|
"1_DAY": "1 Tag",
|
||||||
|
"1_WEEK": "1 Woche"
|
||||||
|
},
|
||||||
|
"SELECT_LIBRARIES_TO_IMPORT": "Bibliotheken zum Importieren auswählen",
|
||||||
|
"SELECT_LIBRARIES_TO_IMPORT_TOOLTIP": "Die Aktivität für Elemente innerhalb dieser Bibliotheken wird weiterhin verfolgt - auch wenn sie nicht importiert werden.",
|
||||||
|
"DATE_ADDED": "Hinzugefügt am"
|
||||||
|
},
|
||||||
|
"TASK_TYPE": {
|
||||||
|
"JOB": "Job",
|
||||||
|
"IMPORT": "Import"
|
||||||
|
},
|
||||||
|
"TASK_DESCRIPTION": {
|
||||||
|
"PartialJellyfinSync": "Synchronisierung kürzlich hinzugefügter Elemente",
|
||||||
|
"JellyfinSync": "Vollständige Synchronisierung mit Jellyfin",
|
||||||
|
"Jellyfin_Playback_Reporting_Plugin_Sync": "Import von Wiedergabeberichts-Plugin-Daten",
|
||||||
|
"Backup": "Jellystat Sicherung"
|
||||||
|
},
|
||||||
|
"ABOUT_PAGE": {
|
||||||
|
"ABOUT_JELLYSTAT": "Über Jellystat",
|
||||||
|
"VERSION": "Version",
|
||||||
|
"UPDATE_AVAILABLE": "Update verfügbar",
|
||||||
|
"GITHUB": "Github",
|
||||||
|
"Backup": "Jellystat Sicherung"
|
||||||
|
},
|
||||||
|
"TIMELINE_PAGE": {
|
||||||
|
"TIMELINE": "Zeitleiste",
|
||||||
|
"EPISODES_one": "Episode",
|
||||||
|
"EPISODES_other": "Episoden"
|
||||||
|
},
|
||||||
|
"SEARCH": "Suchen",
|
||||||
|
"TOTAL": "Gesamt",
|
||||||
|
"LAST": "Letzten",
|
||||||
|
"SERIES": "Serien",
|
||||||
|
"SEASON": "Staffel",
|
||||||
|
"SEASONS": "Staffeln",
|
||||||
|
"EPISODE": "Episode",
|
||||||
|
"EPISODES": "Episoden",
|
||||||
|
"MOVIES": "Filme",
|
||||||
|
"MUSIC": "Musik",
|
||||||
|
"SONGS": "Lieder",
|
||||||
|
"FILES": "Dateien",
|
||||||
|
"LIBRARIES": "Bibliotheken",
|
||||||
|
"USER": "Benutzer",
|
||||||
|
"USERS": "Benutzer",
|
||||||
|
"TYPE": "Typ",
|
||||||
|
"NEW_VERSION_AVAILABLE": "Neue Version verfügbar",
|
||||||
|
"ARCHIVED": "Archiviert",
|
||||||
|
"NOT_ARCHIVED": "Nicht archiviert",
|
||||||
|
"ALL": "Alle",
|
||||||
|
"CLOSE": "Schließen",
|
||||||
|
"TOTAL_PLAYS": "Gesamtwiedergaben",
|
||||||
|
"TITLE": "Titel",
|
||||||
|
"VIEWS": "Ansichten",
|
||||||
|
"WATCH_TIME": "Wiedergabezeit",
|
||||||
|
"LAST_WATCHED": "Zuletzt angesehen",
|
||||||
|
"MEDIA": "Medien",
|
||||||
|
"SAVE": "Speichern",
|
||||||
|
"YES": "Ja",
|
||||||
|
"NO": "Nein",
|
||||||
|
"FILE_NAME": "Dateiname",
|
||||||
|
"DATE": "Datum",
|
||||||
|
"START": "Start",
|
||||||
|
"STOP": "Stop",
|
||||||
|
"DOWNLOAD": "Herunterladen",
|
||||||
|
"RESTORE": "Wiederherstellen",
|
||||||
|
"ACTIONS": "Aktionen",
|
||||||
|
"DELETE": "Löschen",
|
||||||
|
"BITRATE": "Bitrate",
|
||||||
|
"CONTAINER": "Container",
|
||||||
|
"VIDEO": "Video",
|
||||||
|
"CODEC": "Codec",
|
||||||
|
"WIDTH": "Breite",
|
||||||
|
"HEIGHT": "Höhe",
|
||||||
|
"FRAMERATE": "Bildrate",
|
||||||
|
"DYNAMIC_RANGE": "Dynamikbereich",
|
||||||
|
"ASPECT_RATIO": "Seitenverhältnis",
|
||||||
|
"AUDIO": "Audio",
|
||||||
|
"CHANNELS": "Kanäle",
|
||||||
|
"LANGUAGE": "Sprache",
|
||||||
|
"STREAM_DETAILS": "Stream Details",
|
||||||
|
"SOURCE_DETAILS": "Details zur Videoquelle",
|
||||||
|
"DIRECT": "Direkt",
|
||||||
|
"TRANSCODE": "Transkodieren",
|
||||||
|
"DIRECT_STREAM": "Direkt-Stream",
|
||||||
|
"USERNAME": "Benutzername",
|
||||||
|
"PASSWORD": "Passwort",
|
||||||
|
"LOGIN": "Anmelden",
|
||||||
|
"FT_SETUP_PROGRESS": "Erster Einrichtungsschritt {STEP} von {TOTAL}",
|
||||||
|
"VALIDATING": "Validierung läuft",
|
||||||
|
"SAVE_JELLYFIN_DETAILS": "Jellyfin-Details speichern",
|
||||||
|
"SETTINGS_SAVED": "Einstellungen gespeichert",
|
||||||
|
"SUCCESS": "Erfolg",
|
||||||
|
"PASSWORD_UPDATE_SUCCESS": "Passwort erfolgreich aktualisiert",
|
||||||
|
"CREATE_USER": "Benutzer erstellen",
|
||||||
|
"GEOLOCATION_INFO_FOR": "Geolokalisierungsinformationen für",
|
||||||
|
"CITY": "Stadt",
|
||||||
|
"REGION": "Region",
|
||||||
|
"COUNTRY": "Land",
|
||||||
|
"ORGANIZATION": "Organisation",
|
||||||
|
"ISP": "ISP",
|
||||||
|
"LATITUDE": "Breitengrad",
|
||||||
|
"LONGITUDE": "Längengrad",
|
||||||
|
"TIMEZONE": "Zeitzone",
|
||||||
|
"POSTCODE": "Postleitzahl",
|
||||||
|
"X_ROWS_SELECTED": "{ROWS} Zeilen ausgewählt",
|
||||||
|
"TRANSCODE_REASONS": "Transkodierungsgründe",
|
||||||
|
"SUBTITLES": "Untertitel",
|
||||||
|
"GENRES": "Genres"
|
||||||
|
}
|
||||||
@@ -167,7 +167,11 @@
|
|||||||
"STAT_PAGE": {
|
"STAT_PAGE": {
|
||||||
"STATISTICS": "Statistics",
|
"STATISTICS": "Statistics",
|
||||||
"DAILY_PLAY_PER_LIBRARY": "Daily Play Count Per Library",
|
"DAILY_PLAY_PER_LIBRARY": "Daily Play Count Per Library",
|
||||||
"PLAY_COUNT_BY": "Play Count By"
|
"DAILY_DURATION_PER_LIBRARY": "Daily Play Duration Per Library",
|
||||||
|
"PLAY_COUNT_BY": "Play Count By",
|
||||||
|
"PLAY_DURATION_BY": "Play Duration By",
|
||||||
|
"COUNT_VIEW": "Count",
|
||||||
|
"DURATION_VIEW": "Duration"
|
||||||
},
|
},
|
||||||
"SETTINGS_PAGE": {
|
"SETTINGS_PAGE": {
|
||||||
"SETTINGS": "Settings",
|
"SETTINGS": "Settings",
|
||||||
@@ -211,6 +215,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",
|
||||||
@@ -226,7 +239,12 @@
|
|||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"TYPE": "Type",
|
"TYPE": "Type",
|
||||||
"TRIGGER": "Trigger",
|
"TRIGGER": "Trigger",
|
||||||
"STATUS": "Status"
|
"STATUS": "Status",
|
||||||
|
"EVENT_WEBHOOKS": "Event notifications",
|
||||||
|
"EVENT_WEBHOOKS_TOOLTIP": "Enable or disable event notifications",
|
||||||
|
"PLAYBACK_STARTED": "Playback Started",
|
||||||
|
"PLAYBACK_ENDED": "Playback Stopped",
|
||||||
|
"MEDIA_ADDED": "Media Added"
|
||||||
},
|
},
|
||||||
"TASK_TYPE": {
|
"TASK_TYPE": {
|
||||||
"JOB": "Job",
|
"JOB": "Job",
|
||||||
|
|||||||
@@ -211,9 +211,36 @@
|
|||||||
"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",
|
||||||
|
"WEBHOOKS": "Webhooks",
|
||||||
|
"WEBHOOK_TYPE": "Type de webhook",
|
||||||
|
"TEST_NOW": "Tester maintenant",
|
||||||
|
"WEBHOOKS_CONFIGURATION": "Configuration des webhooks",
|
||||||
|
"WEBHOOKS_TOOLTIP": "L'URL des webhooks utiliser pour envoyer des notifications à Discord ou à d'autres services",
|
||||||
|
"WEBHOOK_SAVED": "Webhook sauvegardé",
|
||||||
|
"WEBHOOK_NAME": "Nom du webhook",
|
||||||
|
"DISCORD_WEBHOOK_URL": "URL du webhook Discord",
|
||||||
|
"ENABLE_WEBHOOK": "Activer le webhook",
|
||||||
|
"URL": "URL",
|
||||||
|
"TYPE": "Type",
|
||||||
|
"TRIGGER": "Déclencheur",
|
||||||
|
"STATUS": "Status",
|
||||||
|
"EVENT_WEBHOOKS": "Notifications d'événements",
|
||||||
|
"EVENT_WEBHOOKS_TOOLTIP": "Activez ou désactivez les notifications pour différents événements du système",
|
||||||
|
"PLAYBACK_STARTED": "Lecture commencée",
|
||||||
|
"PLAYBACK_ENDED": "Lecture arrêtée",
|
||||||
|
"MEDIA_ADDED": "Média ajouté"
|
||||||
},
|
},
|
||||||
"TASK_TYPE": {
|
"TASK_TYPE": {
|
||||||
"JOB": "Job",
|
"JOB": "Job",
|
||||||
|
|||||||
@@ -23,4 +23,8 @@ export const languages = [
|
|||||||
id: "ca-ES",
|
id: "ca-ES",
|
||||||
description: "Català",
|
description: "Català",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "de-DE",
|
||||||
|
description: "Deutsch",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import baseUrl from "../../../lib/baseurl";
|
|||||||
import "../../css/timeline/activity-timeline.css";
|
import "../../css/timeline/activity-timeline.css";
|
||||||
|
|
||||||
import { useMediaQuery, useTheme } from "@mui/material";
|
import { useMediaQuery, useTheme } from "@mui/material";
|
||||||
import moment from "moment";
|
import dayjs from "dayjs";
|
||||||
import TvLineIcon from "remixicon-react/TvLineIcon.js";
|
import TvLineIcon from "remixicon-react/TvLineIcon.js";
|
||||||
import FilmLineIcon from "remixicon-react/FilmLineIcon.js";
|
import FilmLineIcon from "remixicon-react/FilmLineIcon.js";
|
||||||
import { MEDIA_TYPES } from "./helpers";
|
import { MEDIA_TYPES } from "./helpers";
|
||||||
@@ -29,8 +29,8 @@ const dateFormatOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function formatEntryDates(FirstActivityDate, LastActivityDate, MediaType) {
|
function formatEntryDates(FirstActivityDate, LastActivityDate, MediaType) {
|
||||||
const startDate = moment(FirstActivityDate);
|
const startDate = dayjs(FirstActivityDate);
|
||||||
const endDate = moment(LastActivityDate);
|
const endDate = dayjs(LastActivityDate);
|
||||||
|
|
||||||
if (startDate.isSame(endDate, "day") || MediaType === MEDIA_TYPES.Movies) {
|
if (startDate.isSame(endDate, "day") || MediaType === MEDIA_TYPES.Movies) {
|
||||||
return Intl.DateTimeFormat(localization, dateFormatOptions).format(
|
return Intl.DateTimeFormat(localization, dateFormatOptions).format(
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ function LibraryItems(props) {
|
|||||||
localStorage.getItem("PREF_sortAsc") != undefined ? localStorage.getItem("PREF_sortAsc") == "true" : true
|
localStorage.getItem("PREF_sortAsc") != undefined ? localStorage.getItem("PREF_sortAsc") == "true" : true
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(sortOrder);
|
|
||||||
|
|
||||||
const archive = {
|
const archive = {
|
||||||
all: "all",
|
all: "all",
|
||||||
archived: "true",
|
archived: "true",
|
||||||
@@ -212,7 +210,11 @@ function LibraryItems(props) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<MoreItemCards data={item} base_url={config.settings?.EXTERNAL_URL ?? config.hostUrl} key={item.Id + item.SeasonNumber + item.EpisodeNumber} />
|
<MoreItemCards
|
||||||
|
data={item}
|
||||||
|
base_url={config.settings?.EXTERNAL_URL ?? config.hostUrl}
|
||||||
|
key={item.Id + item.SeasonNumber + item.EpisodeNumber}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -44,6 +44,20 @@ export default function SettingsConfig() {
|
|||||||
set12hr(Boolean(storage_12hr));
|
set12hr(Boolean(storage_12hr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchAdmins = async () => {
|
||||||
|
try {
|
||||||
|
const adminData = await axios.get(`/proxy/getAdminUsers`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setAdmins(adminData.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Config.getConfig()
|
Config.getConfig()
|
||||||
.then((config) => {
|
.then((config) => {
|
||||||
@@ -59,20 +73,6 @@ export default function SettingsConfig() {
|
|||||||
setsubmissionMessage("Error Retrieving Configuration. Unable to contact Backend Server");
|
setsubmissionMessage("Error Retrieving Configuration. Unable to contact Backend Server");
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchAdmins = async () => {
|
|
||||||
try {
|
|
||||||
const adminData = await axios.get(`/proxy/getAdminUsers`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setAdmins(adminData.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchAdmins();
|
fetchAdmins();
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
@@ -91,6 +91,8 @@ export default function SettingsConfig() {
|
|||||||
console.log("Config updated successfully:", response.data);
|
console.log("Config updated successfully:", response.data);
|
||||||
setisSubmitted("Success");
|
setisSubmitted("Success");
|
||||||
setsubmissionMessage("Successfully updated configuration");
|
setsubmissionMessage("Successfully updated configuration");
|
||||||
|
Config.setConfig();
|
||||||
|
fetchAdmins();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
let errorMessage = error.response.data.errorMessage;
|
let errorMessage = error.response.data.errorMessage;
|
||||||
@@ -98,7 +100,6 @@ export default function SettingsConfig() {
|
|||||||
setisSubmitted("Failed");
|
setisSubmitted("Failed");
|
||||||
setsubmissionMessage(`Error Updating Configuration: ${errorMessage}`);
|
setsubmissionMessage(`Error Updating Configuration: ${errorMessage}`);
|
||||||
});
|
});
|
||||||
Config.setConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFormSubmitExternal(event) {
|
async function handleFormSubmitExternal(event) {
|
||||||
@@ -233,9 +234,13 @@ export default function SettingsConfig() {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
{isSubmitted !== "" ? (
|
{isSubmitted !== "" ? (
|
||||||
isSubmitted === "Failed" ? (
|
isSubmitted === "Failed" ? (
|
||||||
<Alert bg="dark" data-bs-theme="dark" variant="danger">{submissionMessage}</Alert>
|
<Alert bg="dark" data-bs-theme="dark" variant="danger">
|
||||||
|
{submissionMessage}
|
||||||
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Alert bg="dark" data-bs-theme="dark" variant="success">{submissionMessage}</Alert>
|
<Alert bg="dark" data-bs-theme="dark" variant="success">
|
||||||
|
{submissionMessage}
|
||||||
|
</Alert>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
@@ -265,9 +270,13 @@ export default function SettingsConfig() {
|
|||||||
|
|
||||||
{isSubmittedExternal !== "" ? (
|
{isSubmittedExternal !== "" ? (
|
||||||
isSubmittedExternal === "Failed" ? (
|
isSubmittedExternal === "Failed" ? (
|
||||||
<Alert bg="dark" data-bs-theme="dark" variant="danger">{submissionMessageExternal}</Alert>
|
<Alert bg="dark" data-bs-theme="dark" variant="danger">
|
||||||
|
{submissionMessageExternal}
|
||||||
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
<Alert bg="dark" data-bs-theme="dark" variant="success">{submissionMessageExternal}</Alert>
|
<Alert bg="dark" data-bs-theme="dark" variant="success">
|
||||||
|
{submissionMessageExternal}
|
||||||
|
</Alert>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import axios from "../../../lib/axios_instance";
|
|||||||
import { Form, Row, Col, Button, Spinner, Alert } from "react-bootstrap";
|
import { Form, Row, Col, Button, Spinner, Alert } from "react-bootstrap";
|
||||||
import InformationLineIcon from "remixicon-react/InformationLineIcon";
|
import InformationLineIcon from "remixicon-react/InformationLineIcon";
|
||||||
import { Tooltip } from "@mui/material";
|
import { Tooltip } from "@mui/material";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Table from '@mui/material/Table';
|
import Table from '@mui/material/Table';
|
||||||
import TableBody from '@mui/material/TableBody';
|
import TableBody from '@mui/material/TableBody';
|
||||||
@@ -28,16 +29,16 @@ function WebhookRow(props) {
|
|||||||
<TableCell>{webhook.webhook_type || 'generic'}</TableCell>
|
<TableCell>{webhook.webhook_type || 'generic'}</TableCell>
|
||||||
<TableCell>{webhook.trigger_type}</TableCell>
|
<TableCell>{webhook.trigger_type}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`badge ${webhook.enabled ? 'bg-success' : 'bg-secondary'}`}>
|
<span className={`badge ${webhook.enabled ? 'bg-success' : 'bg-secondary'}`}>
|
||||||
{webhook.enabled ? <Trans i18nKey={"ENABLED"} /> : <Trans i18nKey={"DISABLED"} />}
|
{webhook.enabled ? <Trans i18nKey={"ENABLED"} /> : <Trans i18nKey={"DISABLED"} />}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="d-flex justify-content-end gap-2">
|
<div className="d-flex justify-content-end gap-2">
|
||||||
<Button size="sm" variant="outline-primary" onClick={() => onEdit(webhook)}>
|
<Button size="sm" variant="outline-primary" onClick={() => onEdit(webhook)}>
|
||||||
<Trans i18nKey={"EDIT"} />
|
<Trans i18nKey={"EDIT"} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline-secondary" onClick={() => onTest(webhook.id)}>
|
<Button size="sm" variant="outline-secondary" onClick={() => onTest(webhook)}>
|
||||||
<Trans i18nKey={"SETTINGS_PAGE.TEST_NOW"} />
|
<Trans i18nKey={"SETTINGS_PAGE.TEST_NOW"} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,6 +48,19 @@ function WebhookRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebhookRow.propTypes = {
|
||||||
|
webhook: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
webhook_type: PropTypes.string,
|
||||||
|
trigger_type: PropTypes.string.isRequired,
|
||||||
|
enabled: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
onEdit: PropTypes.func.isRequired,
|
||||||
|
onTest: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
function WebhooksSettings() {
|
function WebhooksSettings() {
|
||||||
const [webhooks, setWebhooks] = useState([]);
|
const [webhooks, setWebhooks] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -63,6 +77,12 @@ function WebhooksSettings() {
|
|||||||
webhook_type: 'discord'
|
webhook_type: 'discord'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [eventWebhooks, setEventWebhooks] = useState({
|
||||||
|
playback_started: { exists: false, enabled: false },
|
||||||
|
playback_ended: { exists: false, enabled: false },
|
||||||
|
media_recently_added: { exists: false, enabled: false }
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchWebhooks = async () => {
|
const fetchWebhooks = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -73,18 +93,19 @@ function WebhooksSettings() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data != webhooks) {
|
if (response.data !== webhooks) {
|
||||||
setWebhooks(response.data);
|
setWebhooks(response.data);
|
||||||
}
|
await loadEventWebhooks();
|
||||||
|
}
|
||||||
if (loading) {
|
|
||||||
|
if (loading) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error loading webhooks:", err);
|
console.error("Error loading webhooks:", err);
|
||||||
if (loading) {
|
if (loading) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +113,7 @@ function WebhooksSettings() {
|
|||||||
|
|
||||||
const intervalId = setInterval(fetchWebhooks, 1000 * 10);
|
const intervalId = setInterval(fetchWebhooks, 1000 * 10);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, []);
|
}, [webhooks.length]);
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
@@ -111,7 +132,13 @@ function WebhooksSettings() {
|
|||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
if (!currentWebhook.url) {
|
if (!currentWebhook.url) {
|
||||||
setError("Discord webhook URL is required");
|
setError("Webhook URL is required");
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWebhook.trigger_type === 'event' && !currentWebhook.event_type) {
|
||||||
|
setError("Event type is required for an event based webhook");
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -134,6 +161,17 @@ function WebhooksSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const webhooksResponse = await axios.get('/webhooks', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setWebhooks(webhooksResponse.data);
|
||||||
|
|
||||||
|
await loadEventWebhooks();
|
||||||
|
|
||||||
setCurrentWebhook({
|
setCurrentWebhook({
|
||||||
name: 'New Webhook',
|
name: 'New Webhook',
|
||||||
url: '',
|
url: '',
|
||||||
@@ -143,10 +181,11 @@ function WebhooksSettings() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
webhook_type: 'discord'
|
webhook_type: 'discord'
|
||||||
});
|
});
|
||||||
|
|
||||||
setSuccess("Webhook saved successfully!");
|
setSuccess("Webhook saved successfully!");
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Error during webhook saving: " + (err.response?.data?.error || err.message));
|
setError("Error while saving webhook " + (err.response?.data?.error || err.message));
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -155,9 +194,10 @@ function WebhooksSettings() {
|
|||||||
setCurrentWebhook(webhook);
|
setCurrentWebhook(webhook);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTest = async (webhookId) => {
|
const handleTest = async (webhook) => {
|
||||||
if (!webhookId) {
|
if (!webhook || !webhook.id) {
|
||||||
setError("Impossible to test the webhook: no ID provided");
|
setError("Impossible to test the webhook: no webhook provided");
|
||||||
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,14 +205,20 @@ function WebhooksSettings() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
await axios.post(`/webhooks/${webhookId}/trigger-monthly`, {}, {
|
let endpoint = `/webhooks/${webhook.id}/test`;
|
||||||
|
|
||||||
|
if (webhook.trigger_type === 'scheduled' && webhook.schedule && webhook.schedule.includes('1 * *')) {
|
||||||
|
endpoint = `/webhooks/${webhook.id}/trigger-monthly`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(endpoint, {}, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setSuccess("Webhook test triggered successfully!");
|
setSuccess(`Webhook ${webhook.name} test triggered successfully!`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError("Error during the test of the webhook: " + (err.response?.data?.message || err.message));
|
setError("Error during the test of the webhook: " + (err.response?.data?.message || err.message));
|
||||||
@@ -180,6 +226,107 @@ function WebhooksSettings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEventWebhookStatus = (eventType) => {
|
||||||
|
return eventWebhooks[eventType]?.enabled || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadEventWebhooks = async () => {
|
||||||
|
try {
|
||||||
|
const eventTypes = ['playback_started', 'playback_ended', 'media_recently_added'];
|
||||||
|
const status = {};
|
||||||
|
|
||||||
|
eventTypes.forEach(eventType => {
|
||||||
|
const matchingWebhooks = webhooks.filter(
|
||||||
|
webhook => webhook.trigger_type === 'event' && webhook.event_type === eventType
|
||||||
|
);
|
||||||
|
|
||||||
|
status[eventType] = {
|
||||||
|
exists: matchingWebhooks.length > 0,
|
||||||
|
enabled: matchingWebhooks.some(webhook => webhook.enabled)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setEventWebhooks(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading event webhook status:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEventWebhook = async (eventType) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const isCurrentlyEnabled = getEventWebhookStatus(eventType);
|
||||||
|
const matchingWebhooks = webhooks.filter(
|
||||||
|
webhook => webhook.trigger_type === 'event' && webhook.event_type === eventType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingWebhooks.length === 0 && !isCurrentlyEnabled) {
|
||||||
|
const newWebhook = {
|
||||||
|
name: `Notification - ${getEventDisplayName(eventType)}`,
|
||||||
|
url: '',
|
||||||
|
enabled: true,
|
||||||
|
trigger_type: 'event',
|
||||||
|
event_type: eventType,
|
||||||
|
method: 'POST',
|
||||||
|
webhook_type: 'discord'
|
||||||
|
};
|
||||||
|
|
||||||
|
setCurrentWebhook(newWebhook);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const webhook of matchingWebhooks) {
|
||||||
|
await axios.put(`/webhooks/${webhook.id}`,
|
||||||
|
{ ...webhook, enabled: !isCurrentlyEnabled },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventWebhooks(prev => ({
|
||||||
|
...prev,
|
||||||
|
[eventType]: {
|
||||||
|
...prev[eventType],
|
||||||
|
enabled: !isCurrentlyEnabled
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const response = await axios.get('/webhooks', {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setWebhooks(response.data);
|
||||||
|
setLoading(false);
|
||||||
|
setSuccess(`Webhook for ${getEventDisplayName(eventType)} ${!isCurrentlyEnabled ? 'enabled' : 'disabled'} with success!`);
|
||||||
|
} catch (error) {
|
||||||
|
setError("Error while editing webhook: " + (error.response?.data?.error || error.message));
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventDisplayName = (eventType) => {
|
||||||
|
switch(eventType) {
|
||||||
|
case 'playback_started':
|
||||||
|
return 'Playback started';
|
||||||
|
case 'playback_ended':
|
||||||
|
return 'Playback ended';
|
||||||
|
case 'media_recently_added':
|
||||||
|
return 'New media added';
|
||||||
|
default:
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading && !webhooks.length) {
|
if (loading && !webhooks.length) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
@@ -273,7 +420,72 @@ function WebhooksSettings() {
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
{/* Ajout de la section pour les webhooks événementiels */}
|
||||||
|
<div className="event-webhooks mt-4 mb-4">
|
||||||
|
<h3 className="my-3">
|
||||||
|
<Trans i18nKey={"SETTINGS_PAGE.EVENT_WEBHOOKS"} />
|
||||||
|
<Tooltip title={<Trans i18nKey={"SETTINGS_PAGE.EVENT_WEBHOOKS_TOOLTIP"} />}>
|
||||||
|
<span className="ms-2">
|
||||||
|
<InformationLineIcon />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<Row className="g-4">
|
||||||
|
<Col md={4}>
|
||||||
|
<div className="border rounded p-3 h-25">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5><Trans i18nKey={"SETTINGS_PAGE.PLAYBACK_STARTED"} /></h5>
|
||||||
|
<Form.Check
|
||||||
|
type="switch"
|
||||||
|
id="playback-started-enabled"
|
||||||
|
checked={getEventWebhookStatus('playback_started')}
|
||||||
|
onChange={() => toggleEventWebhook('playback_started')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="small">
|
||||||
|
Send a webhook notification when a user starts watching a media
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={4}>
|
||||||
|
<div className="border rounded p-3 h-25">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5><Trans i18nKey={"SETTINGS_PAGE.PLAYBACK_ENDED"} /></h5>
|
||||||
|
<Form.Check
|
||||||
|
type="switch"
|
||||||
|
id="playback-ended-enabled"
|
||||||
|
checked={getEventWebhookStatus('playback_ended')}
|
||||||
|
onChange={() => toggleEventWebhook('playback_ended')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="small">
|
||||||
|
Send a webhook notification when a user finishes watching a media
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col md={4}>
|
||||||
|
<div className="border rounded p-3 h-25">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5><Trans i18nKey={"SETTINGS_PAGE.MEDIA_ADDED"} /></h5>
|
||||||
|
<Form.Check
|
||||||
|
type="switch"
|
||||||
|
id="media-recently-added-enabled"
|
||||||
|
checked={getEventWebhookStatus('media_recently_added')}
|
||||||
|
onChange={() => toggleEventWebhook('media_recently_added')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="small">
|
||||||
|
Send a webhook notification when new media is added to the library
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TableContainer className='rounded-2 mt-4'>
|
<TableContainer className='rounded-2 mt-4'>
|
||||||
<Table aria-label="webhooks table">
|
<Table aria-label="webhooks table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
@@ -305,7 +517,7 @@ function WebhooksSettings() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,12 +10,26 @@ import i18next from "i18next";
|
|||||||
|
|
||||||
function GenreStatCard(props) {
|
function GenreStatCard(props) {
|
||||||
const [maxRange, setMaxRange] = useState(100);
|
const [maxRange, setMaxRange] = useState(100);
|
||||||
|
const [data, setData] = useState(props.data);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const maxDuration = props.data.reduce((max, item) => {
|
const maxDuration = props.data.reduce((max, item) => {
|
||||||
return Math.max(max, parseFloat((props.dataKey == "duration" ? item.duration : item.plays) || 0));
|
return Math.max(max, parseFloat((props.dataKey == "duration" ? item.duration : item.plays) || 0));
|
||||||
}, 0);
|
}, 0);
|
||||||
setMaxRange(maxDuration);
|
setMaxRange(maxDuration);
|
||||||
|
|
||||||
|
let sorted = [...props.data]
|
||||||
|
.sort((a, b) => {
|
||||||
|
const valueA = parseFloat(props.dataKey === "duration" ? a.duration : a.plays) || 0;
|
||||||
|
const valueB = parseFloat(props.dataKey === "duration" ? b.duration : b.plays) || 0;
|
||||||
|
return valueB - valueA; // Descending order
|
||||||
|
})
|
||||||
|
.slice(0, 15); // Take only the top 10
|
||||||
|
|
||||||
|
// Sort top 10 genres alphabetically
|
||||||
|
sorted = sorted.sort((a, b) => a.genre.localeCompare(b.genre));
|
||||||
|
|
||||||
|
setData(sorted);
|
||||||
}, [props.data, props.dataKey]);
|
}, [props.data, props.dataKey]);
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }) => {
|
const CustomTooltip = ({ active, payload }) => {
|
||||||
@@ -67,7 +81,7 @@ function GenreStatCard(props) {
|
|||||||
</h1>
|
</h1>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={props.data}>
|
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={data}>
|
||||||
<PolarGrid gridType="circle" />
|
<PolarGrid gridType="circle" />
|
||||||
<PolarAngleAxis dataKey="genre" />
|
<PolarAngleAxis dataKey="genre" />
|
||||||
<PolarRadiusAxis domain={[0, maxRange]} tick={false} axisLine={false} />
|
<PolarRadiusAxis domain={[0, maxRange]} tick={false} axisLine={false} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, Legend } from "recharts";
|
import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, Tooltip, Legend } from "recharts";
|
||||||
|
|
||||||
function Chart({ stats, libraries }) {
|
function Chart({ stats, libraries, viewName }) {
|
||||||
const colors = [
|
const colors = [
|
||||||
"rgb(54, 162, 235)", // blue
|
"rgb(54, 162, 235)", // blue
|
||||||
"rgb(255, 99, 132)", // pink
|
"rgb(255, 99, 132)", // pink
|
||||||
@@ -24,13 +24,25 @@ function Chart({ stats, libraries }) {
|
|||||||
"rgb(147, 112, 219)", // medium purple
|
"rgb(147, 112, 219)", // medium purple
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const flattenedStats = stats.map(item => {
|
||||||
|
const flatItem = { Key: item.Key };
|
||||||
|
for (const [libraryName, data] of Object.entries(item)) {
|
||||||
|
if (libraryName === "Key") continue;
|
||||||
|
flatItem[libraryName] = data[viewName] ?? 0;
|
||||||
|
}
|
||||||
|
return flatItem;
|
||||||
|
});
|
||||||
|
|
||||||
const CustomTooltip = ({ payload, label, active }) => {
|
const CustomTooltip = ({ payload, label, active }) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
return (
|
return (
|
||||||
<div style={{ backgroundColor: "rgba(0,0,0,0.8)", color: "white" }} className="p-2 rounded-2 border-0">
|
<div style={{ backgroundColor: "rgba(0,0,0,0.8)", color: "white" }} className="p-2 rounded-2 border-0">
|
||||||
<p className="text-center fs-5">{label}</p>
|
<p className="text-center fs-5">{label}</p>
|
||||||
{libraries.map((library, index) => (
|
{libraries.map((library, index) => (
|
||||||
<p key={library.Id} style={{ color: `${colors[index]}` }}>{`${library.Name} : ${payload[index].value} Views`}</p>
|
// <p key={library.Id} style={{ color: `${colors[index]}` }}>{`${library.Name} : ${payload[index].value} Views`}</p>
|
||||||
|
<p key={library.Id} style={{ color: `${colors[index]}` }}>
|
||||||
|
{`${library.Name} : ${payload?.find(p => p.dataKey === library.Name).value} ${viewName === "count" ? "Views" : "Minutes"}`}
|
||||||
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -41,16 +53,14 @@ function Chart({ stats, libraries }) {
|
|||||||
|
|
||||||
const getMaxValue = () => {
|
const getMaxValue = () => {
|
||||||
let max = 0;
|
let max = 0;
|
||||||
if (stats) {
|
flattenedStats.forEach(datum => {
|
||||||
stats.forEach((datum) => {
|
libraries.forEach(library => {
|
||||||
Object.keys(datum).forEach((key) => {
|
const value = parseFloat(datum[library.Name]);
|
||||||
if (key !== "Key") {
|
if (!isNaN(value)) {
|
||||||
max = Math.max(max, parseInt(datum[key]));
|
max = Math.max(max, value);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
return max;
|
return max;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,7 +68,7 @@ function Chart({ stats, libraries }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%">
|
<ResponsiveContainer width="100%">
|
||||||
<AreaChart data={stats} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
<AreaChart data={flattenedStats} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
||||||
<defs>
|
<defs>
|
||||||
{libraries.map((library, index) => (
|
{libraries.map((library, index) => (
|
||||||
<linearGradient key={library.Id} id={library.Id} x1="0" y1="0" x2="0" y2="1">
|
<linearGradient key={library.Id} id={library.Id} x1="0" y1="0" x2="0" y2="1">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ function DailyPlayStats(props) {
|
|||||||
const [stats, setStats] = useState();
|
const [stats, setStats] = useState();
|
||||||
const [libraries, setLibraries] = useState();
|
const [libraries, setLibraries] = useState();
|
||||||
const [days, setDays] = useState(20);
|
const [days, setDays] = useState(20);
|
||||||
|
const [viewName, setViewName] = useState("count");
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
|
||||||
@@ -17,12 +18,11 @@ function DailyPlayStats(props) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLibraries = () => {
|
const fetchLibraries = () => {
|
||||||
const url = `/stats/getViewsOverTime`;
|
const url = `/stats/getViewsOverTime?days=${props.days}`;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(
|
.get(
|
||||||
url,
|
url,
|
||||||
{ days: props.days },
|
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -46,19 +46,24 @@ function DailyPlayStats(props) {
|
|||||||
setDays(props.days);
|
setDays(props.days);
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
}
|
}
|
||||||
|
if (props.viewName !== viewName) {
|
||||||
|
setViewName(props.viewName);
|
||||||
|
}
|
||||||
|
|
||||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [stats,libraries, days, props.days, token]);
|
}, [stats,libraries, days, props.days, props.viewName, token]);
|
||||||
|
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const titleKey = viewName === "count" ? "STAT_PAGE.DAILY_PLAY_PER_LIBRARY" : "STAT_PAGE.DAILY_DURATION_PER_LIBRARY";
|
||||||
|
|
||||||
if (stats.length === 0) {
|
if (stats.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="main-widget">
|
<div className="main-widget">
|
||||||
<h1><Trans i18nKey={"STAT_PAGE.DAILY_PLAY_PER_LIBRARY"}/> - {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
|
<h1><Trans i18nKey={titleKey}/> - {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
|
||||||
|
|
||||||
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
|
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,10 +71,10 @@ function DailyPlayStats(props) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="main-widget">
|
<div className="main-widget">
|
||||||
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.DAILY_PLAY_PER_LIBRARY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
|
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
|
||||||
|
|
||||||
<div className="graph">
|
<div className="graph">
|
||||||
<Chart libraries={libraries} stats={stats} />
|
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ function PlayStatsByDay(props) {
|
|||||||
const [stats, setStats] = useState();
|
const [stats, setStats] = useState();
|
||||||
const [libraries, setLibraries] = useState();
|
const [libraries, setLibraries] = useState();
|
||||||
const [days, setDays] = useState(20);
|
const [days, setDays] = useState(20);
|
||||||
|
const [viewName, setViewName] = useState("count");
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLibraries = () => {
|
const fetchLibraries = () => {
|
||||||
const url = `/stats/getViewsByDays`;
|
const url = `/stats/getViewsByDays?days=${props.days}`;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(
|
.get(
|
||||||
url,
|
url,
|
||||||
{ days: props.days },
|
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -42,19 +42,24 @@ function PlayStatsByDay(props) {
|
|||||||
setDays(props.days);
|
setDays(props.days);
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
}
|
}
|
||||||
|
if (props.viewName !== viewName) {
|
||||||
|
setViewName(props.viewName);
|
||||||
|
}
|
||||||
|
|
||||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [stats, libraries, days, props.days, token]);
|
}, [stats, libraries, days, props.days, props.viewName, token]);
|
||||||
|
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const titleKey = viewName === "count" ? "STAT_PAGE.PLAY_COUNT_BY" : "STAT_PAGE.PLAY_DURATION_BY";
|
||||||
|
|
||||||
if (stats.length === 0) {
|
if (stats.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="statistics-widget small">
|
<div className="statistics-widget small">
|
||||||
<h1><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
|
<h1><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
|
||||||
|
|
||||||
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
|
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,9 +68,9 @@ function PlayStatsByDay(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="statistics-widget">
|
<div className="statistics-widget">
|
||||||
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
|
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.DAY"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
|
||||||
<div className="graph small">
|
<div className="graph small">
|
||||||
<Chart libraries={libraries} stats={stats} />
|
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ function PlayStatsByHour(props) {
|
|||||||
const [stats, setStats] = useState();
|
const [stats, setStats] = useState();
|
||||||
const [libraries, setLibraries] = useState();
|
const [libraries, setLibraries] = useState();
|
||||||
const [days, setDays] = useState(20);
|
const [days, setDays] = useState(20);
|
||||||
|
const [viewName, setViewName] = useState("count");
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLibraries = () => {
|
const fetchLibraries = () => {
|
||||||
const url = `/stats/getViewsByHour`;
|
const url = `/stats/getViewsByHour?days=${props.days}`;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(
|
.get(
|
||||||
url,
|
url,
|
||||||
{ days: props.days },
|
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -41,19 +41,23 @@ function PlayStatsByHour(props) {
|
|||||||
setDays(props.days);
|
setDays(props.days);
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
}
|
}
|
||||||
|
if (props.viewName !== viewName) {
|
||||||
|
setViewName(props.viewName);
|
||||||
|
}
|
||||||
|
|
||||||
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
const intervalId = setInterval(fetchLibraries, 60000 * 5);
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [stats, libraries, days, props.days, token]);
|
}, [stats, libraries, days, props.days, props.viewName, token]);
|
||||||
|
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const titleKey = viewName === "count" ? "STAT_PAGE.PLAY_COUNT_BY" : "STAT_PAGE.PLAY_DURATION_BY";
|
||||||
if (stats.length === 0) {
|
if (stats.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="statistics-widget small">
|
<div className="statistics-widget small">
|
||||||
<h1><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
|
<h1><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h1>
|
||||||
|
|
||||||
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
|
<h5><Trans i18nKey={"ERROR_MESSAGES.NO_STATS"}/></h5>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,9 +67,9 @@ function PlayStatsByHour(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="statistics-widget">
|
<div className="statistics-widget">
|
||||||
<h2 className="text-start my-2"><Trans i18nKey={"STAT_PAGE.PLAY_COUNT_BY"}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
|
<h2 className="text-start my-2"><Trans i18nKey={titleKey}/> <Trans i18nKey={"UNITS.HOUR"}/> - <Trans i18nKey={"LAST"}/> {days} <Trans i18nKey={`UNITS.DAY${days>1 ? 'S':''}`}/></h2>
|
||||||
<div className="graph small">
|
<div className="graph small">
|
||||||
<Chart libraries={libraries} stats={stats} />
|
<Chart libraries={libraries} stats={stats} viewName={viewName}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,6 +47,17 @@
|
|||||||
margin-bottom: 10px !important;
|
margin-bottom: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stats-tab-nav {
|
||||||
|
background-color: var(--secondary-background-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-canvas {
|
.chart-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 400px;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import SecuritySettings from "./components/settings/security";
|
|||||||
import ApiKeys from "./components/settings/apiKeys";
|
import ApiKeys from "./components/settings/apiKeys";
|
||||||
import WebhooksSettings from "./components/settings/webhooks";
|
import WebhooksSettings from "./components/settings/webhooks";
|
||||||
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";
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export default function Settings() {
|
|||||||
>
|
>
|
||||||
<SettingsConfig />
|
<SettingsConfig />
|
||||||
<SecuritySettings />
|
<SecuritySettings />
|
||||||
|
<ActivityMonitorSettings />
|
||||||
<Tasks />
|
<Tasks />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Tabs, Tab } from "react-bootstrap";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import "./css/stats.css";
|
import "./css/stats.css";
|
||||||
@@ -20,6 +21,13 @@ function Statistics() {
|
|||||||
localStorage.setItem("PREF_STATISTICS_STAT_DAYS_INPUT", event.target.value);
|
localStorage.setItem("PREF_STATISTICS_STAT_DAYS_INPUT", event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(localStorage.getItem(`PREF_STATISTICS_LAST_SELECTED_TAB`) ?? "tabCount");
|
||||||
|
|
||||||
|
function setTab(tabName) {
|
||||||
|
setActiveTab(tabName);
|
||||||
|
localStorage.setItem(`PREF_STATISTICS_LAST_SELECTED_TAB`, tabName);
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
if (input < 1) {
|
if (input < 1) {
|
||||||
@@ -43,6 +51,26 @@ function Statistics() {
|
|||||||
<h1>
|
<h1>
|
||||||
<Trans i18nKey={"STAT_PAGE.STATISTICS"} />
|
<Trans i18nKey={"STAT_PAGE.STATISTICS"} />
|
||||||
</h1>
|
</h1>
|
||||||
|
<div className="stats-tab-nav">
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey={activeTab}
|
||||||
|
activeKey={activeTab}
|
||||||
|
onSelect={setTab}
|
||||||
|
variant="pills"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
eventKey="tabCount"
|
||||||
|
className="bg-transparent"
|
||||||
|
title={<Trans i18nKey="STAT_PAGE.COUNT_VIEW" />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
eventKey="tabDuration"
|
||||||
|
className="bg-transparent"
|
||||||
|
title={<Trans i18nKey="STAT_PAGE.DURATION_VIEW" />}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
<div className="date-range">
|
<div className="date-range">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<Trans i18nKey={"LAST"} />
|
<Trans i18nKey={"LAST"} />
|
||||||
@@ -55,14 +83,26 @@ function Statistics() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<DailyPlayStats days={days} />
|
|
||||||
|
|
||||||
<div className="statistics-graphs">
|
{activeTab === "tabCount" && (
|
||||||
<PlayStatsByDay days={days} />
|
<div>
|
||||||
<PlayStatsByHour days={days} />
|
<DailyPlayStats days={days} viewName="count" />
|
||||||
|
<div className="statistics-graphs">
|
||||||
|
<PlayStatsByDay days={days} viewName="count" />
|
||||||
|
<PlayStatsByHour days={days} viewName="count" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{activeTab === "tabDuration" && (
|
||||||
|
<div>
|
||||||
|
<DailyPlayStats days={days} viewName="duration" />
|
||||||
|
<div className="statistics-graphs">
|
||||||
|
<PlayStatsByDay days={days} viewName="duration" />
|
||||||
|
<PlayStatsByHour days={days} viewName="duration" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user