mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Create backup function
Create backup feature to store data as a json file Modified Terminal to be more dynamic in the backend to be used by multiple modules, messages are now stored and send from server side instead of in the session. Added username to last watched items in library activity fixed navbar home url Reverted stats to ut nor rely on jf_all_playback_activity as it has not been implemented yet
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
const WebSocket = require('ws');
|
||||
|
||||
function createWebSocketServer(port) {
|
||||
const wss = new WebSocket.Server({ port });
|
||||
function createWebSocketServer() {
|
||||
var messages=[];
|
||||
const port=process.env.WS_PORT || 3004 ;
|
||||
const wss = new WebSocket.Server({port});
|
||||
|
||||
// function to handle WebSocket connections
|
||||
|
||||
function handleConnection(ws) {
|
||||
console.log('Client connected');
|
||||
|
||||
@@ -17,24 +18,30 @@ function createWebSocketServer(port) {
|
||||
ws.on('close', () => {
|
||||
console.log('Client disconnected');
|
||||
});
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify(messages));
|
||||
}
|
||||
|
||||
// call the handleConnection function for each new WebSocket connection
|
||||
wss.on('connection', handleConnection);
|
||||
|
||||
// define a separate method that sends a message to all connected clients
|
||||
function sendMessageToClients(message) {
|
||||
messages.push(message);
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
let parsedMessage = JSON.stringify(message);
|
||||
console.log(parsedMessage);
|
||||
client.send(parsedMessage);
|
||||
client.send(JSON.stringify(messages));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sendMessageToClients;
|
||||
function clearMessages() {
|
||||
messages=[];
|
||||
}
|
||||
|
||||
return {sendMessageToClients, clearMessages, wss};
|
||||
}
|
||||
|
||||
module.exports = createWebSocketServer;
|
||||
const wsServer = createWebSocketServer();
|
||||
|
||||
module.exports = wsServer;
|
||||
|
||||
@@ -187,7 +187,7 @@ router.get("/getHistory", async (req, res) => {
|
||||
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT * FROM jf_playback_activity order by "ActivityDateInserted" desc`
|
||||
`SELECT * FROM jf_all_playback_activity order by "ActivityDateInserted" desc`
|
||||
);
|
||||
|
||||
const groupedResults = {};
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
const { Router } = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
const path = require('path');
|
||||
const moment = require('moment');
|
||||
|
||||
const wss = require("./WebsocketHandler");
|
||||
|
||||
var messages=[];
|
||||
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -13,7 +19,7 @@ const postgresPort = process.env.POSTGRES_PORT;
|
||||
const postgresDatabase = process.env.POSTGRES_DATABASE || 'jfstat';
|
||||
|
||||
// Tables to back up
|
||||
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity'];
|
||||
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity','jf_playback_reporting_plugin_data','jf_item_info'];
|
||||
|
||||
// Backup function
|
||||
async function backup() {
|
||||
@@ -26,19 +32,25 @@ async function backup() {
|
||||
});
|
||||
|
||||
// Get data from each table and append it to the backup file
|
||||
const backupPath = './backup-data/backup.json';
|
||||
let now = moment();
|
||||
const backupPath = `./backup-data/backup_${now.format('yyyy-MM-DD HH-mm-ss')}.json`;
|
||||
const stream = fs.createWriteStream(backupPath, { flags: 'a' });
|
||||
const backup_data=[];
|
||||
wss.clearMessages();
|
||||
wss.sendMessageToClients({ color: "yellow", Message: "Begin Backup "+backupPath });
|
||||
for (let table of tables) {
|
||||
const query = `SELECT * FROM ${table}`;
|
||||
|
||||
const { rows } = await pool.query(query);
|
||||
console.log(`Reading ${rows.length} rows for table ${table}`);
|
||||
wss.sendMessageToClients({color: "dodgerblue",Message: `Saving ${rows.length} rows for table ${table}`});
|
||||
|
||||
backup_data.push({[table]:rows});
|
||||
backup_data.push({[table]:rows});
|
||||
// stream.write(JSON.stringify(backup_data));
|
||||
|
||||
}
|
||||
|
||||
wss.sendMessageToClients({ color: "lawngreen", Message: "Backup Complete" });
|
||||
stream.write(JSON.stringify(backup_data));
|
||||
stream.end();
|
||||
|
||||
@@ -151,6 +163,55 @@ router.get('/restore', async (req, res) => {
|
||||
res.status(500).send('Backup failed');
|
||||
}
|
||||
});
|
||||
|
||||
//list backup files
|
||||
const backupfolder='backup-data';
|
||||
|
||||
|
||||
router.get('/files', (req, res) => {
|
||||
const directoryPath = path.join(__dirname, backupfolder);
|
||||
fs.readdir(directoryPath, (err, files) => {
|
||||
if (err) {
|
||||
res.status(500).send('Unable to read directory');
|
||||
} else {
|
||||
const fileData = files.map(file => {
|
||||
const filePath = path.join(directoryPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
name: file,
|
||||
size: stats.size,
|
||||
datecreated: stats.birthtime
|
||||
};
|
||||
});
|
||||
res.json(fileData);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//download backup file
|
||||
router.get('/files/:filename', (req, res) => {
|
||||
const filePath = path.join(__dirname, backupfolder, req.params.filename);
|
||||
res.download(filePath);
|
||||
});
|
||||
|
||||
//delete backup
|
||||
router.delete('/files/:filename', (req, res) => {
|
||||
const filePath = path.join(__dirname, backupfolder, req.params.filename);
|
||||
|
||||
fs.unlink(filePath, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('An error occurred while deleting the file.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${filePath} has been deleted.`);
|
||||
res.status(200).send(`${filePath} has been deleted.`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ app.use('/auth', authRouter); // mount the API router at /api, with JWT middlewa
|
||||
app.use('/api', verifyToken, apiRouter); // mount the API router at /api, with JWT middleware
|
||||
app.use('/sync', verifyToken, syncRouter); // mount the API router at /sync, with JWT middleware
|
||||
app.use('/stats', verifyToken, statsRouter); // mount the API router at /stats, with JWT middleware
|
||||
app.use('/data', backupRouter); // mount the API router at /stats, with JWT middleware
|
||||
app.use('/data', verifyToken, backupRouter); // mount the API router at /stats, with JWT middleware
|
||||
|
||||
try{
|
||||
createdb.createDatabase().then((result) => {
|
||||
|
||||
@@ -419,7 +419,7 @@ router.post("/getGlobalItemStats", async (req, res) => {
|
||||
const { rows } = await db.query(
|
||||
`select count(*)"Plays",
|
||||
sum("PlaybackDuration") total_playback_duration
|
||||
from jf_playback_activity
|
||||
from jf_all_playback_activity jf_playback_activity
|
||||
where
|
||||
("EpisodeId"='${itemid}' OR "SeasonId"='${itemid}' OR "NowPlayingItemId"='${itemid}')
|
||||
AND jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - INTERVAL '1 hour' * ${_hours} AND NOW();`
|
||||
|
||||
@@ -3,8 +3,9 @@ const pgp = require("pg-promise")();
|
||||
const db = require("./db");
|
||||
const axios = require("axios");
|
||||
|
||||
const ws = require("./WebsocketHandler");
|
||||
const sendMessageToClients = ws(process.env.WS_PORT || 3004);
|
||||
const wss = require("./WebsocketHandler");
|
||||
const socket=wss;
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -134,7 +135,7 @@ async function syncUserData()
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
sendMessageToClients({ Message: "Error: Config details not found!" });
|
||||
socket.sendMessageToClients({ Message: "Error: Config details not found!" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,9 +161,9 @@ async function syncUserData()
|
||||
if (dataToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_users",dataToInsert,jf_users_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
socket.sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -173,9 +174,9 @@ async function syncUserData()
|
||||
if (toDeleteIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_users",toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(toDeleteIds.length + " Rows Removed.");
|
||||
socket.sendMessageToClients(toDeleteIds.length + " Rows Removed.");
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -187,7 +188,7 @@ async function syncLibraryFolders()
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
sendMessageToClients({ Message: "Error: Config details not found!" });
|
||||
socket.sendMessageToClients({ Message: "Error: Config details not found!" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,9 +214,9 @@ async function syncLibraryFolders()
|
||||
if (dataToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_libraries",dataToInsert,jf_libraries_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
socket.sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -226,9 +227,9 @@ async function syncLibraryFolders()
|
||||
if (toDeleteIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_libraries",toDeleteIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
sendMessageToClients(toDeleteIds.length + " Rows Removed.");
|
||||
socket.sendMessageToClients(toDeleteIds.length + " Rows Removed.");
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -244,9 +245,9 @@ async function syncLibraryItems()
|
||||
}
|
||||
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/3" });
|
||||
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/3" });
|
||||
|
||||
sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",});
|
||||
socket.sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",});
|
||||
|
||||
const admins = await _sync.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
@@ -289,7 +290,7 @@ async function syncLibraryItems()
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertCounter += dataToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -302,23 +303,23 @@ async function syncLibraryItems()
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteCounter +=toDeleteIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
}
|
||||
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",});
|
||||
sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
|
||||
socket.sendMessageToClients({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",});
|
||||
socket.sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",});
|
||||
socket.sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
|
||||
|
||||
// const { rows: cleanup } = await db.query('DELETE FROM jf_playback_activity where "NowPlayingItemId" not in (select "Id" from jf_library_items)' );
|
||||
// sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
|
||||
// socket.sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
|
||||
|
||||
}
|
||||
|
||||
async function syncShowItems()
|
||||
{
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/3" });
|
||||
sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",});
|
||||
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/3" });
|
||||
socket.sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",});
|
||||
|
||||
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
|
||||
@@ -387,7 +388,7 @@ async function syncShowItems()
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertSeasonsCount += seasonsToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -400,7 +401,7 @@ async function syncShowItems()
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteSeasonsCount +=toDeleteIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -411,7 +412,7 @@ async function syncShowItems()
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertEpisodeCount += episodesToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -425,25 +426,25 @@ async function syncShowItems()
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteEpisodeCount +=toDeleteEpisodeIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sendMessageToClients({ Message: "Sync complete for " + show.Name });
|
||||
socket.sendMessageToClients({ Message: "Sync complete for " + show.Name });
|
||||
}
|
||||
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
|
||||
sendMessageToClients({ color: "yellow", Message: "Sync Complete" });
|
||||
socket.sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",});
|
||||
socket.sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
|
||||
socket.sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
|
||||
socket.sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
|
||||
socket.sendMessageToClients({ color: "yellow", Message: "Sync Complete" });
|
||||
}
|
||||
|
||||
async function syncItemInfo()
|
||||
{
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
|
||||
sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
|
||||
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
|
||||
socket.sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
|
||||
|
||||
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
|
||||
@@ -485,7 +486,7 @@ async function syncItemInfo()
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertItemInfoCount += ItemInfoToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -498,7 +499,7 @@ async function syncItemInfo()
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteItemInfoCount +=toDeleteItemInfoIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -529,7 +530,7 @@ async function syncItemInfo()
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertEpisodeInfoCount += EpisodeInfoToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
socket.sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
@@ -542,24 +543,24 @@ async function syncItemInfo()
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteEpisodeInfoCount +=toDeleteEpisodeInfoIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
socket.sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
console.log(Episode.Name)
|
||||
}
|
||||
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",});
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes Info inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Info Removed.",});
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
|
||||
socket.sendMessageToClients({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted.",});
|
||||
socket.sendMessageToClients({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",});
|
||||
socket.sendMessageToClients({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes Info inserted.",});
|
||||
socket.sendMessageToClients({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Info Removed.",});
|
||||
socket.sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
|
||||
}
|
||||
|
||||
async function syncPlaybackPluginData()
|
||||
{
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
|
||||
sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
|
||||
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
|
||||
socket.sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
|
||||
|
||||
try {
|
||||
const { rows: config } = await db.query(
|
||||
@@ -624,12 +625,15 @@ async function syncPlaybackPluginData()
|
||||
|
||||
///////////////////////////////////////Sync All
|
||||
router.get("/beingSync", async (req, res) => {
|
||||
socket.clearMessages();
|
||||
|
||||
await syncUserData();
|
||||
await syncLibraryFolders();
|
||||
await syncLibraryItems();
|
||||
await syncShowItems();
|
||||
await syncItemInfo();
|
||||
|
||||
|
||||
res.send();
|
||||
|
||||
});
|
||||
|
||||
@@ -55,6 +55,13 @@ function LastWatchedCard(props) {
|
||||
<div className="last-last-played">
|
||||
{formatTime(props.data.LastPlayed)}
|
||||
</div>
|
||||
|
||||
<div className="pb-2">
|
||||
<Link to={`/users/${props.data.UserId}`}>
|
||||
{props.data.UserName}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
<div className="last-item-episode"> {props.data.EpisodeName}</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function Navbar() {
|
||||
return (
|
||||
<BootstrapNavbar variant="dark" expand="md" className="navbar py-0">
|
||||
<Container fluid>
|
||||
<BootstrapNavbar.Brand href="#home">Jellystat</BootstrapNavbar.Brand>
|
||||
<BootstrapNavbar.Brand as={Link} to={"/"}>Jellystat</BootstrapNavbar.Brand>
|
||||
<BootstrapNavbar.Toggle aria-controls="responsive-navbar-nav" />
|
||||
<BootstrapNavbar.Collapse id="responsive-navbar-nav">
|
||||
<Nav className="ms-auto">
|
||||
|
||||
@@ -12,7 +12,7 @@ const TerminalComponent = () => {
|
||||
// handle incoming messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
let message = JSON.parse(event.data);
|
||||
setMessages(prevMessages => [...prevMessages, message]);
|
||||
setMessages(message);
|
||||
});
|
||||
|
||||
// cleanup function to close the WebSocket connection when the component unmounts
|
||||
|
||||
163
src/pages/components/settings/backupfiles.js
Normal file
163
src/pages/components/settings/backupfiles.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { useState,useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
|
||||
|
||||
|
||||
import "../../css/settings/backups.css";
|
||||
import { Table } from "react-bootstrap";
|
||||
|
||||
|
||||
export default function BackupFiles() {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [showAlert, setshowAlert] = useState({visible:false,type:'danger',title:'Error',message:''});
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const backupFiles = await axios.get(`/data/files`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
setFiles(backupFiles.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [files,token]);
|
||||
|
||||
|
||||
|
||||
async function downloadBackup(filename) {
|
||||
const url=`/data/files/${filename}`;
|
||||
axios({
|
||||
url: url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
}).then(response => {
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode.removeChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteBackup(filename) {
|
||||
const url=`/data/files/${filename}`;
|
||||
axios
|
||||
.delete(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
setshowAlert({visible:true,title:'Success',type:'success',message:response.data});
|
||||
})
|
||||
.catch((error) => {
|
||||
setshowAlert({visible:true,title:'Error',type:'danger',message:error.response.data});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
function formatFileSize(sizeInBytes) {
|
||||
const sizeInKB = sizeInBytes / 1024; // 1 KB = 1024 bytes
|
||||
if (sizeInKB < 1024) {
|
||||
return `${sizeInKB.toFixed(2)} KB`;
|
||||
} else {
|
||||
const sizeInMB = sizeInKB / 1024; // 1 MB = 1024 KB
|
||||
if (sizeInMB < 1024) {
|
||||
return `${sizeInMB.toFixed(2)} MB`;
|
||||
} else {
|
||||
const sizeInGB = sizeInMB / 1024; // 1 GB = 1024 MB
|
||||
if (sizeInGB < 1024) {
|
||||
return `${sizeInGB.toFixed(2)} GB`;
|
||||
} else {
|
||||
const sizeInTB = sizeInGB / 1024; // 1 TB = 1024 GB
|
||||
if (sizeInTB < 1024) {
|
||||
return `${sizeInTB.toFixed(2)} TB`;
|
||||
} else {
|
||||
const sizeInPB = sizeInTB / 1024; // 1 PB = 1024 TB
|
||||
return `${sizeInPB.toFixed(2)} PB`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const options = {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
function handleCloseAlert() {
|
||||
setshowAlert({visible:false});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="my-2">Backups</h1>
|
||||
{showAlert && showAlert.visible && (
|
||||
<Alert variant={showAlert.type} onClose={handleCloseAlert} dismissible>
|
||||
<Alert.Heading>{showAlert.title}</Alert.Heading>
|
||||
<p>
|
||||
{showAlert.message}
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>File Name</th>
|
||||
<th>Date Created</th>
|
||||
<th>Size</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files &&
|
||||
files.sort((a, b) =>new Date(b.datecreated) - new Date(a.datecreated)).map((file, index) => (
|
||||
<tr key={index}>
|
||||
<td>{file.name}</td>
|
||||
<td>{Intl.DateTimeFormat('en-UK', options).format(new Date(file.datecreated))}</td>
|
||||
<td>{formatFileSize(file.size)}</td>
|
||||
<td ><Button type="button" onClick={()=>downloadBackup(file.name)} >Download</Button></td>
|
||||
<td ><Button type="button" className="btn-danger" onClick={()=>deleteBackup(file.name)} >Delete</Button></td>
|
||||
</tr>
|
||||
))}
|
||||
{files.length===0 ? <tr><td colSpan="5" style={{ textAlign: "center", fontStyle: "italic" ,color:"gray"}}>No Backups Found</td></tr> :''}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import Row from 'react-bootstrap/Row';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
|
||||
|
||||
import "../../css/settings.css";
|
||||
import "../../css/settings/settings.css";
|
||||
|
||||
export default function LibrarySync() {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
@@ -35,6 +35,30 @@ export default function LibrarySync() {
|
||||
// return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
async function createBackup() {
|
||||
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
await axios
|
||||
.get("/data/backup", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
// isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
setProcessing(false);
|
||||
// return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
|
||||
beginSync();
|
||||
@@ -51,7 +75,18 @@ export default function LibrarySync() {
|
||||
</Form.Label>
|
||||
|
||||
<Col sm="10">
|
||||
<Button variant={!processing ? "outline-primary" : "outline-light"} disabled={processing} onClick={handleClick}>Run Sync</Button>
|
||||
<Button variant={!processing ? "outline-primary" : "outline-light"} disabled={processing} onClick={handleClick}>Start</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="mb-3">
|
||||
|
||||
<Form.Label column sm="2">
|
||||
Create Backup
|
||||
</Form.Label>
|
||||
|
||||
<Col sm="10">
|
||||
<Button variant={!processing ? "outline-primary" : "outline-light"} disabled={processing} onClick={createBackup}>Start</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Alert from 'react-bootstrap/Alert';
|
||||
|
||||
|
||||
|
||||
import "../../css/settings.css";
|
||||
import "../../css/settings/settings.css";
|
||||
import { ButtonGroup } from "react-bootstrap";
|
||||
|
||||
export default function SettingsConfig() {
|
||||
|
||||
22
src/pages/css/settings/backups.css
Normal file
22
src/pages/css/settings/backups.css
Normal file
@@ -0,0 +1,22 @@
|
||||
tr{
|
||||
color: white;
|
||||
}
|
||||
|
||||
th:hover{
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
th{
|
||||
border-bottom: none !important;
|
||||
cursor: default !important;
|
||||
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
|
||||
.backup-file-download
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
td{
|
||||
border-bottom: none !important;
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
/* margin: 25px 0; */
|
||||
font-size: 0.9em;
|
||||
font-family: sans-serif;
|
||||
min-width: 400px;
|
||||
/* min-width: 400px; */
|
||||
/* box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); */
|
||||
background-color: rgba(100,100, 100, 0.2);
|
||||
color: white;
|
||||
@@ -20,16 +20,27 @@
|
||||
|
||||
}
|
||||
|
||||
th,
|
||||
|
||||
td
|
||||
{
|
||||
padding: 15px 15px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 576px) {
|
||||
td[data-cell]::before {
|
||||
content: attr(data-cell)": ";
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
color: #a8a8a8;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
td a{
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
@@ -43,16 +54,24 @@ td:hover a{
|
||||
|
||||
|
||||
th {
|
||||
padding: 15px 15px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-bottom: 1px solid transparent !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th:hover {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: rgba(100, 100, 100, 0.1);
|
||||
}
|
||||
|
||||
tr:nth-child(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* tbody tr:last-of-type {
|
||||
border-bottom: 2px solid #009879;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ import React from "react";
|
||||
import SettingsConfig from "./components/settings/settingsConfig";
|
||||
import LibrarySync from "./components/settings/librarySync";
|
||||
|
||||
import BackupFiles from "./components/settings/backupfiles";
|
||||
|
||||
import TerminalComponent from "./components/settings/TerminalComponent";
|
||||
|
||||
import "./css/settings.css";
|
||||
|
||||
|
||||
import "./css/settings/settings.css";
|
||||
|
||||
export default function Settings() {
|
||||
|
||||
@@ -13,7 +17,9 @@ export default function Settings() {
|
||||
return (
|
||||
<div>
|
||||
<SettingsConfig/>
|
||||
<BackupFiles/>
|
||||
<LibrarySync/>
|
||||
|
||||
<TerminalComponent/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -155,19 +155,19 @@ function Users() {
|
||||
<table className="user-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th onClick={() => handleSort("UserName")}>User</th>
|
||||
<th onClick={() => handleSort("LastWatched")}>Last Watched</th>
|
||||
<th onClick={() => handleSort("LastClient")}>Last Client</th>
|
||||
<th onClick={() => handleSort("TotalPlays")}>Total Plays</th>
|
||||
<th onClick={() => handleSort("TotalWatchTime")}>Total Watch Time</th>
|
||||
<th onClick={() => handleSort("LastSeen")}>Last Seen</th>
|
||||
<th className="d-none d-md-table-cell" ></th>
|
||||
<th className="d-none d-md-table-cell" onClick={() => handleSort("UserName")}>User</th>
|
||||
<th className="d-none d-md-table-cell" onClick={() => handleSort("LastWatched")}>Last Watched</th>
|
||||
<th className="d-none d-md-table-cell" onClick={() => handleSort("LastClient")}>Last Client</th>
|
||||
<th className="d-none d-md-table-cell" onClick={() => handleSort("TotalPlays")}>Total Plays</th>
|
||||
<th className="d-none d-md-table-cell" onClick={() => handleSort("TotalWatchTime")}>Total Watch Time</th>
|
||||
<th className="d-none d-md-table-cell" onClick={() => handleSort("LastSeen")}>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentUsers.map((item) => (
|
||||
<tr key={item.UserId}>
|
||||
<td>
|
||||
<tr key={item.UserId} >
|
||||
<td className="d-block d-md-table-cell">
|
||||
{item.PrimaryImageTag ? (
|
||||
<img
|
||||
className="card-user-image"
|
||||
@@ -183,12 +183,12 @@ function Users() {
|
||||
<AccountCircleFillIcon color="#fff" size={30} />
|
||||
)}
|
||||
</td>
|
||||
<td> <Link to={`/users/${item.UserId}`}>{item.UserName}</Link></td>
|
||||
<td>{item.LastWatched || 'never'}</td>
|
||||
<td>{item.LastClient || 'n/a'}</td>
|
||||
<td>{item.TotalPlays}</td>
|
||||
<td>{formatTotalWatchTime(item.TotalWatchTime) || 0}</td>
|
||||
<td>{item.LastSeen ? formatLastSeenTime(item.LastSeen) : 'never'}</td>
|
||||
<td className="d-block d-md-table-cell py-2" data-cell={"User"}> <Link to={`/users/${item.UserId}`}>{item.UserName}</Link></td>
|
||||
<td className="d-block d-md-table-cell py-2" data-cell={"Last Watched"}>{item.LastWatched || 'never'}</td>
|
||||
<td className="d-block d-md-table-cell py-2" data-cell={"Last Client"}>{item.LastClient || 'n/a'}</td>
|
||||
<td className="d-block d-md-table-cell py-2" data-cell={"Total Plays"}>{item.TotalPlays}</td>
|
||||
<td className="d-block d-md-table-cell py-2" data-cell={"Total Watch Time"}>{formatTotalWatchTime(item.TotalWatchTime) || 0}</td>
|
||||
<td className="d-block d-md-table-cell py-2" data-cell={"Last Seen"}>{item.LastSeen ? formatLastSeenTime(item.LastSeen) : 'never'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -29,6 +29,13 @@ module.exports = function(app) {
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
'/data',
|
||||
createProxyMiddleware({
|
||||
target: 'http://127.0.0.1:3003',
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
'/ws',
|
||||
createProxyMiddleware({
|
||||
|
||||
Reference in New Issue
Block a user