From 3a046619151337b34f046af9b324666b41372692 Mon Sep 17 00:00:00 2001 From: Thegan Govender Date: Thu, 25 May 2023 07:21:24 +0200 Subject: [PATCH] Automated Tasks + Logging +misc New: Added automated tasks for Sync and Backups - Backups run every 24hrs, Sync runs every 10 minutes Added switcher to switch between 12hr/24hr time formats Added sorting to Activity Tables Added Version Checking and indicators Added logging on Tasks + Log Page Added Recently Added view to Home page Added background images to item banners Added Proxy for Device Images in session card Changed Navbar to be a side bar Fixes: Fixed Jellyfin API returning Empty folder as library item CSS File to add breakpoints to bootstrap-width Other: Various CSS changes Temporarily removed Websockets due to Proxy Errors Changed Activity View Playback conversion function to more accurately display Playback Duration Backend changes to sum Playback Durations to show more accurate information in thee collapsed summarized view --- .gitignore | 1 + backend/api.js | 24 +- backend/backup.js | 50 ++++- backend/db.js | 2 +- backend/logging.js | 37 +++ backend/migrations/030_jf_logging_table.js | 29 +++ backend/models/jf_logging.js | 26 +++ backend/proxy.js | 41 +++- backend/server.js | 16 +- backend/stats.js | 6 +- backend/sync.js | 191 +++++++++++----- .../{watchdog => tasks}/ActivityMonitor.js | 0 backend/tasks/BackupTask.js | 62 ++++++ backend/tasks/SyncTask.js | 35 +++ backend/version-control.js | 38 ++-- package-lock.json | 10 +- package.json | 3 +- src/App.css | 11 +- src/App.js | 9 +- src/index.css | 8 + src/lib/navdata.js | 18 +- src/pages/about.js | 91 ++++++++ .../components/activity/activity-table.js | 200 ++++++++++++++--- .../components/general/last-watched-card.js | 2 +- src/pages/components/general/navbar.js | 68 +++--- src/pages/components/general/version-card.js | 71 ++++++ src/pages/components/item-info.js | 22 +- .../item-info/more-items/more-items-card.js | 2 +- src/pages/components/library-info.js | 4 +- .../RecentlyAdded/recently-added-card.js | 2 +- src/pages/components/library/library-card.js | 2 +- src/pages/components/library/library-items.js | 5 +- .../components/library/recently-added.js | 32 +-- src/pages/components/libraryOverview.js | 6 +- .../libraryStatCard/library-stat-component.js | 3 +- src/pages/components/sessions/session-card.js | 24 +- src/pages/components/sessions/sessions.js | 5 +- src/pages/components/settings/Tasks.js | 108 +++++++++ .../components/settings/TerminalComponent.js | 27 +-- src/pages/components/settings/backupfiles.js | 5 +- src/pages/components/settings/librarySync.js | 97 -------- src/pages/components/settings/logs.js | 210 ++++++++++++++++++ .../components/settings/settingsConfig.js | 50 ++++- .../components/statCards/ItemStatComponent.js | 4 +- .../components/statCards/most_active_users.js | 13 +- src/pages/css/about.css | 20 ++ src/pages/css/activity/activity-table.css | 12 +- src/pages/css/home.css | 1 + src/pages/css/items/item-details.css | 7 +- src/pages/css/lastplayed.css | 3 +- src/pages/css/library/libraries.css | 4 +- src/pages/css/library/library-card.css | 5 +- src/pages/css/library/media-items.css | 4 +- src/pages/css/libraryOverview.css | 9 +- src/pages/css/loading.css | 4 +- src/pages/css/navbar.css | 82 +++++-- src/pages/css/sessions.css | 3 + src/pages/css/settings/backups.css | 2 +- src/pages/css/settings/settings.css | 19 +- src/pages/css/settings/version.css | 46 ++++ src/pages/css/setup.css | 4 +- src/pages/css/statCard.css | 12 +- src/pages/css/users/user-details.css | 1 + src/pages/css/variables.module.css | 3 +- src/pages/css/websocket/websocket.css | 1 + src/pages/css/width_breakpoint_css.css | 142 ++++++++++++ src/pages/home.js | 5 +- src/pages/libraries.js | 6 +- src/pages/settings.js | 30 ++- src/pages/testing.js | 4 +- src/pages/users.js | 205 +++++++++++++++-- src/setupProxy.js | 7 + 72 files changed, 1880 insertions(+), 431 deletions(-) create mode 100644 backend/logging.js create mode 100644 backend/migrations/030_jf_logging_table.js create mode 100644 backend/models/jf_logging.js rename backend/{watchdog => tasks}/ActivityMonitor.js (100%) create mode 100644 backend/tasks/BackupTask.js create mode 100644 backend/tasks/SyncTask.js create mode 100644 src/pages/about.js create mode 100644 src/pages/components/general/version-card.js create mode 100644 src/pages/components/settings/Tasks.js delete mode 100644 src/pages/components/settings/librarySync.js create mode 100644 src/pages/components/settings/logs.js create mode 100644 src/pages/css/about.css create mode 100644 src/pages/css/settings/version.css create mode 100644 src/pages/css/width_breakpoint_css.css diff --git a/.gitignore b/.gitignore index f21eebd..58d4d79 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ # testing /coverage /backend/backup-data +.vscode # production /build diff --git a/backend/api.js b/backend/api.js index b096d7f..c576e90 100644 --- a/backend/api.js +++ b/backend/api.js @@ -1,9 +1,10 @@ // api.js const express = require("express"); const axios = require("axios"); -const ActivityMonitor=require('./watchdog/ActivityMonitor'); +const ActivityMonitor=require('./tasks/ActivityMonitor'); const db = require("./db"); const https = require('https'); +const { checkForUpdates } = require('./version-control'); const agent = new https.Agent({ rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true' @@ -61,6 +62,19 @@ router.post("/setconfig", async (req, res) => { console.log(`ENDPOINT CALLED: /setconfig: `); }); +router.get("/CheckForUpdates", async (req, res) => { + try{ + + let result=await checkForUpdates(); + res.send(result); + + }catch(error) + { + console.log(error); + } + +}); + router.get("/getLibraries", async (req, res) => { try{ @@ -219,6 +233,14 @@ router.get("/getHistory", async (req, res) => { groupedResults[row.NowPlayingItemId+row.EpisodeId].results.push(row); } }); + + // Update GroupedResults with playbackDurationSum + Object.values(groupedResults).forEach(row => { + if (row.results && row.results.length > 0) { + row.PlaybackDuration = row.results.reduce((acc, item) => acc + parseInt(item.PlaybackDuration), 0); + } + }); + res.send(Object.values(groupedResults)); diff --git a/backend/backup.js b/backend/backup.js index 19e2cec..4301159 100644 --- a/backend/backup.js +++ b/backend/backup.js @@ -3,9 +3,11 @@ const { Pool } = require('pg'); const fs = require('fs'); const path = require('path'); const moment = require('moment'); +const { randomUUID } = require('crypto'); const multer = require('multer'); -const wss = require("./WebsocketHandler"); +// const wss = require("./WebsocketHandler"); +const Logging =require('./logging'); const router = Router(); @@ -21,7 +23,8 @@ const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_lib // Backup function -async function backup() { +async function backup(logData,result) { + logData.push({ color: "lawngreen", Message: "Starting Backup" }); const pool = new Pool({ user: postgresUser, password: postgresPassword, @@ -39,20 +42,19 @@ async function backup() { const backupPath = `./backup-data/backup_${now.format('yyyy-MM-DD HH-mm-ss')}.json`; const stream = fs.createWriteStream(backupPath, { flags: 'a' }); stream.on('error', (error) => { - console.error(error); - wss.sendMessageToClients({ color: "red", Message: "Backup Failed: "+error }); + logData.push({ color: "red", Message: "Backup Failed: "+error }); + result='Failed'; throw new Error(error); }); const backup_data=[]; - wss.clearMessages(); - wss.sendMessageToClients({ color: "yellow", Message: "Begin Backup "+backupPath }); + logData.push({ 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}`}); + logData.push({color: "dodgerblue",Message: `Saving ${rows.length} rows for table ${table}`}); backup_data.push({[table]:rows}); @@ -61,12 +63,13 @@ async function backup() { await stream.write(JSON.stringify(backup_data)); stream.end(); - wss.sendMessageToClients({ color: "lawngreen", Message: "Backup Complete" }); + logData.push({ color: "lawngreen", Message: "Backup Complete" }); }catch(error) { console.log(error); - wss.sendMessageToClients({ color: "red", Message: "Backup Failed: "+error }); + logData.push({ color: "red", Message: "Backup Failed: "+error }); + result='Failed'; } @@ -161,7 +164,28 @@ async function restore(file) { // Route handler for backup endpoint router.get('/backup', async (req, res) => { try { - await backup(); + let startTime = moment(); + let logData=[]; + let result='Success'; + await backup(logData,result); + + let endTime = moment(); + let diffInSeconds = endTime.diff(startTime, 'seconds'); + const uuid = randomUUID(); + const log= + { + "Id":uuid, + "Name":"Backup", + "Type":"Task", + "ExecutionType":"Manual", + "Duration":diffInSeconds, + "TimeRun":startTime, + "Log":JSON.stringify(logData), + "Result": result + + }; + + Logging.insertLog(log); res.send('Backup completed successfully'); } catch (error) { console.error(error); @@ -272,4 +296,8 @@ router.get('/restore/:filename', async (req, res) => { -module.exports = router; +module.exports = +{ + router, + backup +}; diff --git a/backend/db.js b/backend/db.js index f8f5810..d4861bd 100644 --- a/backend/db.js +++ b/backend/db.js @@ -81,7 +81,7 @@ async function insertBulk(table_name, data,columns) { await client.query("COMMIT"); - message=(data.length + " Rows Inserted."); + message=((data.length||1) + " Rows Inserted."); } catch (error) { await client.query('ROLLBACK'); diff --git a/backend/logging.js b/backend/logging.js new file mode 100644 index 0000000..36a9300 --- /dev/null +++ b/backend/logging.js @@ -0,0 +1,37 @@ + +const db = require("./db"); + + + +const {jf_logging_columns,jf_logging_mapping,} = require("./models/jf_logging"); +const express = require("express"); +const router = express.Router(); + +router.get("/getLogs", async (req, res) => { + try { + const { rows } = await db.query(`SELECT * FROM jf_logging order by "TimeRun" desc LIMIT 50 `); + res.send(rows); + } catch (error) { + res.send(error); + } +}); + + +async function insertLog(logItem) +{ + try { + + + await db.insertBulk("jf_logging",logItem,jf_logging_columns); + // console.log(result); + + } catch (error) { + console.log(error); + return []; + } + +} + + +module.exports = +{router,insertLog}; diff --git a/backend/migrations/030_jf_logging_table.js b/backend/migrations/030_jf_logging_table.js new file mode 100644 index 0000000..e45a741 --- /dev/null +++ b/backend/migrations/030_jf_logging_table.js @@ -0,0 +1,29 @@ +exports.up = async function(knex) { + try { + const hasTable = await knex.schema.hasTable('jf_logging'); + if (!hasTable) { + await knex.schema.createTable('jf_logging', function(table) { + table.text('Id').primary(); + table.text('Name').notNullable(); + table.text('Type').notNullable(); + table.text('ExecutionType'); + table.text('Duration').notNullable(); + table.timestamp('TimeRun').defaultTo(knex.fn.now()); + table.json('Log'); + table.text('Result'); + }); + await knex.raw(`ALTER TABLE jf_logging OWNER TO ${process.env.POSTGRES_USER};`); + } + } catch (error) { + console.error(error); + } + }; + + exports.down = async function(knex) { + try { + await knex.schema.dropTableIfExists('jf_logging'); + } catch (error) { + console.error(error); + } + }; + \ No newline at end of file diff --git a/backend/models/jf_logging.js b/backend/models/jf_logging.js new file mode 100644 index 0000000..63f602a --- /dev/null +++ b/backend/models/jf_logging.js @@ -0,0 +1,26 @@ + const jf_logging_columns = [ + "Id", + "Name", + "Type", + "ExecutionType", + "Duration", + "TimeRun", + "Log", + "Result" + ]; + + const jf_logging_mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + Type: item.Type, + ExecutionType: item.ExecutionType, + Duration: item.Duration, + TimeRun: item.TimeRun, + Log: item.Log, + Result: item.Result, + }); + + module.exports = { + jf_logging_columns, + jf_logging_mapping, + }; \ No newline at end of file diff --git a/backend/proxy.js b/backend/proxy.js index 0c7e8ad..17933bf 100644 --- a/backend/proxy.js +++ b/backend/proxy.js @@ -14,8 +14,43 @@ const axios_instance = axios.create({ const router = express.Router(); +router.get('/web/assets/img/devices/', async(req, res) => { + const { devicename } = req.query; // Get the image URL from the query string + const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1'); + + if (config[0].JF_HOST === null || config[0].JF_API_KEY === null || devicename===undefined) { + res.send({ error: "Config Details Not Found" }); + return; + } + + let url=`${config[0].JF_HOST}/web/assets/img/devices/${devicename}.svg`; + + axios_instance.get(url, { + responseType: 'arraybuffer' + }) + .then((response) => { + res.set('Content-Type', 'image/svg+xml'); + res.status(200); + + if (response.headers['content-type'].startsWith('image/')) { + res.send(response.data); + } else { + res.send(response.data.toString()); + } + + return; // Add this line + }) + .catch((error) => { + console.error(error); + res.status(500).send('Error fetching image'); + }); + +}); + + + router.get('/Items/Images/Backdrop/', async(req, res) => { - const { id,fillWidth,quality } = req.query; // Get the image URL from the query string + const { id,fillWidth,quality,blur } = req.query; // Get the image URL from the query string const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1'); if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) { @@ -24,7 +59,7 @@ router.get('/Items/Images/Backdrop/', async(req, res) => { } - let url=`${config[0].JF_HOST}/Items/${id}/Images/Backdrop?fillWidth=${fillWidth || 100}&quality=${quality || 100}`; + let url=`${config[0].JF_HOST}/Items/${id}/Images/Backdrop?fillWidth=${fillWidth || 800}&quality=${quality || 100}&blur=${blur || 0}`; axios_instance.get(url, { @@ -56,7 +91,7 @@ router.get('/Items/Images/Backdrop/', async(req, res) => { } - let url=`${config[0].JF_HOST}/Items/${id}/Images/Primary?fillWidth=${fillWidth || 100}&quality=${quality || 100}`; + let url=`${config[0].JF_HOST}/Items/${id}/Images/Primary?fillWidth=${fillWidth || 400}&quality=${quality || 100}`; axios_instance.get(url, { responseType: 'arraybuffer' diff --git a/backend/server.js b/backend/server.js index 81e9d3d..31bb906 100644 --- a/backend/server.js +++ b/backend/server.js @@ -8,12 +8,13 @@ const knexConfig = require('./migrations'); const authRouter= require('./auth'); const apiRouter = require('./api'); const proxyRouter = require('./proxy'); -const syncRouter = require('./sync'); +const {router: syncRouter} = require('./sync'); const statsRouter = require('./stats'); -const backupRouter = require('./backup'); -const ActivityMonitor = require('./watchdog/ActivityMonitor'); - -const { checkForUpdates } = require('./version-control'); +const {router: backupRouter} = require('./backup'); +const ActivityMonitor = require('./tasks/ActivityMonitor'); +const SyncTask = require('./tasks/SyncTask'); +const BackupTask = require('./tasks/BackupTask'); +const {router: logRouter} = require('./logging'); @@ -57,6 +58,7 @@ app.use('/proxy', proxyRouter); // mount the API router at /api, with JWT middle 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', verifyToken, backupRouter); // mount the API router at /stats, with JWT middleware +app.use('/logs', verifyToken, logRouter); // mount the API router at /stats, with JWT middleware try{ createdb.createDatabase().then((result) => { @@ -67,8 +69,10 @@ try{ db.migrate.latest().then(() => { app.listen(PORT, async () => { console.log(`Server listening on http://${LISTEN_IP}:${PORT}`); - checkForUpdates(); + ActivityMonitor.ActivityMonitor(1000); + SyncTask.SyncTask(60000*10); + BackupTask.BackupTask(60000*60*24); }); }); }); diff --git a/backend/stats.js b/backend/stats.js index 78e7880..78b7c49 100644 --- a/backend/stats.js +++ b/backend/stats.js @@ -308,7 +308,11 @@ router.get("/getRecentlyAdded", async (req, res) => { ); - let url=`${config[0].JF_HOST}/users/${adminUser[0].Id}/Items/latest?parentId=${libraryid}`; + let url=`${config[0].JF_HOST}/users/${adminUser[0].Id}/Items/latest`; + if(libraryid) + { + url+=`?parentId=${libraryid}`; + } const response_data = await axios.get(url, { headers: { diff --git a/backend/sync.js b/backend/sync.js index 158bd7f..2f93061 100644 --- a/backend/sync.js +++ b/backend/sync.js @@ -4,6 +4,8 @@ const db = require("./db"); const axios = require("axios"); const https = require('https'); +const logging=require("./logging"); + const agent = new https.Agent({ rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true' }); @@ -17,6 +19,9 @@ const axios_instance = axios.create({ const wss = require("./WebsocketHandler"); const socket=wss; +const moment = require('moment'); +const { randomUUID } = require('crypto'); + const router = express.Router(); @@ -39,7 +44,6 @@ class sync { async getUsers() { try { const url = `${this.hostUrl}/Users`; - console.log("getAdminUser: ", url); const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, @@ -55,7 +59,6 @@ class sync { async getAdminUser() { try { const url = `${this.hostUrl}/Users`; - console.log("getAdminUser: ", url); const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, @@ -90,7 +93,7 @@ class sync { ["tvshows", "movies","music"].includes(type.CollectionType) ); } else { - return results; + return results.filter((item)=> item.ImageTags.Primary); } } catch (error) { console.log(error); @@ -141,12 +144,13 @@ class sync { } ////////////////////////////////////////API Methods -async function syncUserData() +async function syncUserData(loggedData,result) { 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" }); - socket.sendMessageToClients({ Message: "Error: Config details not found!" }); + loggedData.push({ Message: "Error: Config details not found!" }); + result='Failed'; return; } @@ -172,12 +176,13 @@ async function syncUserData() if (dataToInsert.length !== 0) { let result = await db.insertBulk("jf_users",dataToInsert,jf_users_columns); if (result.Result === "SUCCESS") { - socket.sendMessageToClients(dataToInsert.length + " Rows Inserted."); + loggedData.push(dataToInsert.length + " Rows Inserted."); } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } @@ -185,21 +190,24 @@ async function syncUserData() if (toDeleteIds.length > 0) { let result = await db.deleteBulk("jf_users",toDeleteIds); if (result.Result === "SUCCESS") { - socket.sendMessageToClients(toDeleteIds.length + " Rows Removed."); + loggedData.push(toDeleteIds.length + " Rows Removed."); } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } + } -async function syncLibraryFolders() +async function syncLibraryFolders(loggedData,result) { 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" }); - socket.sendMessageToClients({ Message: "Error: Config details not found!" }); + loggedData.push({ Message: "Error: Config details not found!" }); + result='Failed'; return; } @@ -225,12 +233,13 @@ async function syncLibraryFolders() if (dataToInsert.length !== 0) { let result = await db.insertBulk("jf_libraries",dataToInsert,jf_libraries_columns); if (result.Result === "SUCCESS") { - socket.sendMessageToClients(dataToInsert.length + " Rows Inserted."); + loggedData.push(dataToInsert.length + " Rows Inserted."); } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } @@ -238,27 +247,28 @@ async function syncLibraryFolders() if (toDeleteIds.length > 0) { let result = await db.deleteBulk("jf_libraries",toDeleteIds); if (result.Result === "SUCCESS") { - socket.sendMessageToClients(toDeleteIds.length + " Rows Removed."); + loggedData.push(toDeleteIds.length + " Rows Removed."); } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } } -async function syncLibraryItems() +async function syncLibraryItems(loggedData,result) { const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1' ); if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) { res.send({ error: "Config Details Not Found" }); + result='Failed'; return; } const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); - socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/3" }); - - socket.sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",}); + loggedData.push({ color: "lawngreen", Message: "Syncing... 1/3" }); + loggedData.push({color: "yellow",Message: "Beginning Library Item Sync",}); const admins = await _sync.getAdminUser(); const userid = admins[0].Id; @@ -301,10 +311,11 @@ async function syncLibraryItems() if (result.Result === "SUCCESS") { insertCounter += dataToInsert.length; } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } @@ -314,28 +325,28 @@ async function syncLibraryItems() if (result.Result === "SUCCESS") { deleteCounter +=toDeleteIds.length; } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } - 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" }); + loggedData.push({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",}); + loggedData.push({color: "orange",Message: deleteCounter + " Library Items Removed.",}); + loggedData.push({ 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)' ); - // socket.sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" }); } -async function syncShowItems() +async function syncShowItems(loggedData,result) { - socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/3" }); - socket.sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",}); + loggedData.push({ color: "lawngreen", Message: "Syncing... 2/3" }); + loggedData.push({color: "yellow", Message: "Beginning Seasons and Episode sync",}); const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1'); if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) { res.send({ error: "Config Details Not Found" }); + result='Failed'; return; } @@ -353,8 +364,7 @@ async function syncShowItems() const allSeasons = await _sync.getSeasonsAndEpisodes(show.Id,'Seasons'); const allEpisodes =await _sync.getSeasonsAndEpisodes(show.Id,'Episodes'); show_counter++; - socket.sendMessageToClients({ Message: "Syncing shows " + (show_counter/shows.length*100).toFixed(2) +"%" ,key:'show_sync'}); - + loggedData.push({ Message: "Syncing shows " + (show_counter/shows.length*100).toFixed(2) +"%" ,key:'show_sync'}); const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id)); @@ -401,10 +411,11 @@ async function syncShowItems() if (result.Result === "SUCCESS") { insertSeasonsCount += seasonsToInsert.length; } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } const toDeleteIds = existingIdsSeasons.filter((id) =>!allSeasons.some((row) => row.Id === id )); @@ -414,7 +425,8 @@ async function syncShowItems() if (result.Result === "SUCCESS") { deleteSeasonsCount +=toDeleteIds.length; } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } @@ -425,10 +437,11 @@ async function syncShowItems() if (result.Result === "SUCCESS") { insertEpisodeCount += episodesToInsert.length; } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } @@ -439,7 +452,8 @@ async function syncShowItems() if (result.Result === "SUCCESS") { deleteEpisodeCount +=toDeleteEpisodeIds.length; } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } @@ -447,22 +461,23 @@ async function syncShowItems() } - 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" }); + loggedData.push({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",}); + loggedData.push({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",}); + loggedData.push({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",}); + loggedData.push({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",}); + loggedData.push({ color: "yellow", Message: "Sync Complete" }); } -async function syncItemInfo() +async function syncItemInfo(loggedData,result) { - socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" }); - socket.sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",}); + loggedData.push({ color: "lawngreen", Message: "Syncing... 3/3" }); + loggedData.push({color: "yellow", Message: "Beginning File Info Sync",}); const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1'); if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) { res.send({ error: "Config Details Not Found" }); + result='Failed'; return; } @@ -499,10 +514,11 @@ async function syncItemInfo() if (result.Result === "SUCCESS") { insertItemInfoCount += ItemInfoToInsert.length; } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } const toDeleteItemInfoIds = existingItemInfo.filter((id) =>!data.some((row) => row.Id === id )); @@ -512,14 +528,14 @@ async function syncItemInfo() if (result.Result === "SUCCESS") { deleteItemInfoCount +=toDeleteItemInfoIds.length; } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } } //loop for each Episode - console.log("Episode") for (const Episode of Episodes) { const data = await _sync.getItemInfo(Episode.EpisodeId,userid); @@ -543,10 +559,11 @@ async function syncItemInfo() if (result.Result === "SUCCESS") { insertEpisodeInfoCount += EpisodeInfoToInsert.length; } else { - socket.sendMessageToClients({ + loggedData.push({ color: "red", Message: "Error performing bulk insert:" + result.message, }); + result='Failed'; } } const toDeleteEpisodeInfoIds = existingEpisodeItemInfo.filter((id) =>!data.some((row) => row.Id === id )); @@ -556,18 +573,19 @@ async function syncItemInfo() if (result.Result === "SUCCESS") { deleteEpisodeInfoCount +=toDeleteEpisodeInfoIds.length; } else { - socket.sendMessageToClients({color: "red",Message: result.message,}); + loggedData.push({color: "red",Message: result.message,}); + result='Failed'; } } console.log(Episode.Name) } - 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" }); + loggedData.push({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted.",}); + loggedData.push({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",}); + loggedData.push({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes Info inserted.",}); + loggedData.push({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Info Removed.",}); + loggedData.push({ color: "lawngreen", Message: "Sync Complete" }); } async function syncPlaybackPluginData() @@ -633,20 +651,74 @@ async function syncPlaybackPluginData() } +async function fullSync() +{ + let startTime = moment(); + let loggedData=[]; + let result='Success'; + await syncUserData(loggedData,result); + await syncLibraryFolders(loggedData,result); + await syncLibraryItems(loggedData,result); + await syncShowItems(loggedData,result); + await syncItemInfo(loggedData,result); + const uuid = randomUUID(); + + let endTime = moment(); + + let diffInSeconds = endTime.diff(startTime, 'seconds'); + + const log= + { + "Id":uuid, + "Name":"Jellyfin Sync", + "Type":"Task", + "ExecutionType":"Automatic", + "Duration":diffInSeconds, + "TimeRun":startTime, + "Log":JSON.stringify(loggedData), + "Result":result + + }; + logging.insertLog(log); + + +} + ////////////////////////////////////////API Calls ///////////////////////////////////////Sync All router.get("/beingSync", async (req, res) => { socket.clearMessages(); + let loggedData=[]; + let result='Success'; - await syncUserData(); - await syncLibraryFolders(); - await syncLibraryItems(); - await syncShowItems(); - await syncItemInfo(); + let startTime = moment(); + await syncUserData(loggedData,result); + await syncLibraryFolders(loggedData,result); + await syncLibraryItems(loggedData,result); + await syncShowItems(loggedData,result); + await syncItemInfo(loggedData,result); + const uuid = randomUUID(); + let endTime = moment(); + let diffInSeconds = endTime.diff(startTime, 'seconds'); + + const log= + { + "Id":uuid, + "Name":"Jellyfin Sync", + "Type":"Task", + "ExecutionType":"Manual", + "Duration":diffInSeconds, + "TimeRun":startTime, + "Log":JSON.stringify(loggedData), + "Result":result + + }; + + logging.insertLog(log); res.send(); }); @@ -702,4 +774,5 @@ router.get("/syncPlaybackPluginData", async (req, res) => { -module.exports = router; +module.exports = +{router,fullSync}; diff --git a/backend/watchdog/ActivityMonitor.js b/backend/tasks/ActivityMonitor.js similarity index 100% rename from backend/watchdog/ActivityMonitor.js rename to backend/tasks/ActivityMonitor.js diff --git a/backend/tasks/BackupTask.js b/backend/tasks/BackupTask.js new file mode 100644 index 0000000..2d20ad1 --- /dev/null +++ b/backend/tasks/BackupTask.js @@ -0,0 +1,62 @@ +const db = require("../db"); +const Logging = require("../logging"); + +const backup = require("../backup"); +const moment = require('moment'); +const { randomUUID } = require('crypto'); + + +async function BackupTask(interval) { + console.log("Backup Interval: " + interval); + + + setInterval(async () => { + try { + const { rows: config } = await db.query( + 'SELECT * FROM app_config where "ID"=1' + ); + + + + if (config.length===0 || config[0].JF_HOST === null || config[0].JF_API_KEY === null) { + return; + } + + + + let startTime = moment(); + let logData=[]; + let result='Success'; + + await backup.backup(logData,result); + + let endTime = moment(); + let diffInSeconds = endTime.diff(startTime, 'seconds'); + const uuid = randomUUID(); + const log= + { + "Id":uuid, + "Name":"Backup", + "Type":"Task", + "ExecutionType":"Automatic", + "Duration":diffInSeconds, + "TimeRun":startTime, + "Log":JSON.stringify(logData), + "Result":result + + }; + Logging.insertLog(log); + + + + + } catch (error) { + // console.log(error); + return []; + } + }, interval); +} + +module.exports = { + BackupTask, +}; diff --git a/backend/tasks/SyncTask.js b/backend/tasks/SyncTask.js new file mode 100644 index 0000000..d45b27e --- /dev/null +++ b/backend/tasks/SyncTask.js @@ -0,0 +1,35 @@ +const db = require("../db"); + +const sync = require("../sync"); + +async function SyncTask(interval) { + console.log("LibraryMonitor Interval: " + interval); + + + setInterval(async () => { + try { + const { rows: config } = await db.query( + 'SELECT * FROM app_config where "ID"=1' + ); + + + + if (config.length===0 || config[0].JF_HOST === null || config[0].JF_API_KEY === null) { + return; + } + + sync.fullSync(); + + + + + } catch (error) { + // console.log(error); + return []; + } + }, interval); +} + +module.exports = { + SyncTask, +}; diff --git a/backend/version-control.js b/backend/version-control.js index 6f03a7a..99a774e 100644 --- a/backend/version-control.js +++ b/backend/version-control.js @@ -1,33 +1,43 @@ const GitHub = require('github-api'); const packageJson = require('../package.json'); +const {compareVersions} =require('compare-versions'); async function checkForUpdates() { const currentVersion = packageJson.version; const repoOwner = 'cyfershepard'; - const repoName = 'jellystat'; + const repoName = 'Jellystat'; const gh = new GitHub(); - const repo = gh.getRepo(repoOwner, repoName); + + let result={current_version: packageJson.version, latest_version:'', message:'', update_available:false}; + let latestVersion; try { - const releases = await repo.listReleases(); + const path = 'package.json'; - if (releases.data.length > 0) { - latestVersion = releases.data[0].tag_name; - console.log(releases.data); + const response = await gh.getRepo(repoOwner, repoName).getContents('main', path); + const content = response.data.content; + const decodedContent = Buffer.from(content, 'base64').toString(); + latestVersion = JSON.parse(decodedContent).version; + + if (compareVersions(latestVersion,currentVersion) > 0) { + // console.log(`A new version V.${latestVersion} of ${repoName} is available.`); + result = { current_version: packageJson.version, latest_version: latestVersion, message: `${repoName} has an update ${latestVersion}`, update_available:true }; + } else if (compareVersions(latestVersion,currentVersion) < 0) { + // console.log(`${repoName} is using a beta version.`); + result = { current_version: packageJson.version, latest_version: latestVersion, message: `${repoName} is using a beta version`, update_available:false }; + } else { + // console.log(`${repoName} is up to date.`); + result = { current_version: packageJson.version, latest_version: latestVersion, message: `${repoName} is up to date`, update_available:false }; } } catch (error) { console.error(`Failed to fetch releases for ${repoName}: ${error.message}`); + result = { current_version: packageJson.version, latest_version: 'N/A', message: `Failed to fetch releases for ${repoName}: ${error.message}`, update_available:false }; } - if (latestVersion && latestVersion !== currentVersion) { - console.log(`A new version (${latestVersion}) of ${repoName} is available.`); - } else if (latestVersion) { - console.log(`${repoName} is up to date.`); - } - else { - console.log(`Unable to retrieve latest version`); - } + return result; } + + module.exports = { checkForUpdates }; diff --git a/package-lock.json b/package-lock.json index 2f04145..08a3779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jfstat", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jfstat", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", @@ -22,6 +22,7 @@ "antd": "^5.3.0", "axios": "^1.3.4", "bootstrap": "^5.2.3", + "compare-versions": "^6.0.0-rc.1", "concurrently": "^7.6.0", "cors": "^2.8.5", "crypto-js": "^4.1.1", @@ -7150,6 +7151,11 @@ "version": "1.0.1", "license": "MIT" }, + "node_modules/compare-versions": { + "version": "6.0.0-rc.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.0.0-rc.1.tgz", + "integrity": "sha512-cFhkjbGY1jLFWIV7KegECbfuyYPxSGvgGkdkfM+ibboQDoPwg2FRHm5BSNTOApiauRBzJIQH7qvOJs2sW5ueKQ==" + }, "node_modules/compressible": { "version": "2.0.18", "license": "MIT", diff --git a/package.json b/package.json index ebd0ff7..b233535 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jfstat", - "version": "0.1.0", + "version": "1.0.0", "private": true, "dependencies": { "@emotion/react": "^11.10.6", @@ -17,6 +17,7 @@ "antd": "^5.3.0", "axios": "^1.3.4", "bootstrap": "^5.2.3", + "compare-versions": "^6.0.0-rc.1", "concurrently": "^7.6.0", "cors": "^2.8.5", "crypto-js": "^4.1.1", diff --git a/src/App.css b/src/App.css index 3a51e0d..ce1939a 100644 --- a/src/App.css +++ b/src/App.css @@ -1,8 +1,11 @@ @import 'pages/css/variables.module.css'; main{ - margin-inline: 20px; + margin-inline: 20px; + /* width: 100%; */ + overflow: auto; } + .App-logo { height: 40vmin; pointer-events: none; @@ -82,26 +85,24 @@ h2{ .btn-outline-primary { - color: grey!important; + color: white!important; border-color: var(--primary-color) !important; + background-color: var(--background-color) !important; } .btn-outline-primary:hover { - color: white !important; background-color: var(--primary-color) !important; } .btn-outline-primary.active { - color: white !important; background-color: var(--primary-color) !important; } .btn-outline-primary:focus { - color: white !important; background-color: var(--primary-color) !important; } diff --git a/src/App.js b/src/App.js index 8e42c16..64b104c 100644 --- a/src/App.js +++ b/src/App.js @@ -22,6 +22,7 @@ import Libraries from './pages/libraries'; import LibraryInfo from './pages/components/library-info'; import ItemInfo from './pages/components/item-info'; import ErrorPage from './pages/components/general/error'; +import About from './pages/about'; import Testing from './pages/testing'; @@ -116,9 +117,10 @@ if (config && config.apiKey ===null) { if (config && isConfigured && token!==null){ return (
- -
-
+ +
+ +
} /> } /> @@ -130,6 +132,7 @@ if (config && isConfigured && token!==null){ } /> } /> } /> + } />
diff --git a/src/index.css b/src/index.css index b23a4ff..f823fdb 100644 --- a/src/index.css +++ b/src/index.css @@ -14,6 +14,14 @@ body { color: white ; } +* +{ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',sans-serif !important; + + +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; diff --git a/src/lib/navdata.js b/src/lib/navdata.js index da09e70..ef87ef3 100644 --- a/src/lib/navdata.js +++ b/src/lib/navdata.js @@ -7,6 +7,7 @@ import HistoryFillIcon from 'remixicon-react/HistoryFillIcon'; import SettingsFillIcon from 'remixicon-react/SettingsFillIcon'; import GalleryFillIcon from 'remixicon-react/GalleryFillIcon'; import UserFillIcon from 'remixicon-react/UserFillIcon'; +import InformationFillIcon from 'remixicon-react/InformationFillIcon'; // import ReactjsFillIcon from 'remixicon-react/ReactjsFillIcon'; @@ -16,7 +17,7 @@ export const navData = [ id: 0, icon: , text: "Home", - link: "/" + link: "" }, { id: 1, @@ -49,13 +50,14 @@ export const navData = [ text: "Settings", link: "settings" } + , + + { + id: 7, + icon: , + text: "About", + link: "about" + } ] -// { -// id: 5, -// icon: , -// text: "Component Testing Playground", -// link: "testing" -// } -// , \ No newline at end of file diff --git a/src/pages/about.js b/src/pages/about.js new file mode 100644 index 0000000..a9f0be6 --- /dev/null +++ b/src/pages/about.js @@ -0,0 +1,91 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +// import Button from "react-bootstrap/Button"; +// import Card from 'react-bootstrap/Card'; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; +import Loading from "./components/general/loading"; + + +import "./css/about.css"; +import { Card } from "react-bootstrap"; + +export default function SettingsAbout() { + + const token = localStorage.getItem('token'); + const [data, setData] = useState(); + useEffect(() => { + + const fetchVersion = () => { + if (token) { + const url = `/api/CheckForUpdates`; + + axios + .get(url, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + if(!data) + { + fetchVersion(); + } + + const intervalId = setInterval(fetchVersion, 60000 * 5); + return () => clearInterval(intervalId); + }, [token]); + + + if(!data) + { + return ; + } + + + return ( +
+

About Jellystat

+ + + + + Version: + + + {data.current_version} + + + + + Update Available: + + + {data.message} + + + + + + Github: + + + https://github.com/CyferShepard/Jellystat + + + + +
+ ); + + +} diff --git a/src/pages/components/activity/activity-table.js b/src/pages/components/activity/activity-table.js index 5e5c374..878d69f 100644 --- a/src/pages/components/activity/activity-table.js +++ b/src/pages/components/activity/activity-table.js @@ -10,43 +10,46 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Collapse from '@mui/material/Collapse'; +import TableSortLabel from '@mui/material/TableSortLabel'; import IconButton from '@mui/material/IconButton'; import Box from '@mui/material/Box'; +import { visuallyHidden } from '@mui/utils'; + import AddCircleFillIcon from 'remixicon-react/AddCircleFillIcon'; import IndeterminateCircleFillIcon from 'remixicon-react/IndeterminateCircleFillIcon'; import '../../css/activity/activity-table.css'; // localStorage.setItem('hour12',true); -let hour_format = Boolean(localStorage.getItem('hour12')); + function formatTotalWatchTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; - const hours = Math.floor(seconds / 3600); // 1 hour = 3600 seconds - const minutes = Math.floor((seconds % 3600) / 60); // 1 minute = 60 seconds - let formattedTime=''; - if(hours) - { - formattedTime+=`${hours} hours`; - } - if(minutes) - { - formattedTime+=` ${minutes} minutes`; + let timeString = ''; + + if (hours > 0) { + timeString += `${hours} ${hours === 1 ? 'hr' : 'hrs'} `; } - if(!hours && !minutes) - { - // const seconds = Math.floor(((seconds % 3600) / 60) / 60); // 1 minute = 60 seconds - formattedTime+=` ${seconds} seconds`; + if (minutes > 0) { + timeString += `${minutes} ${minutes === 1 ? 'min' : 'mins'} `; } - return formattedTime ; + if (remainingSeconds > 0) { + timeString += `${remainingSeconds} ${remainingSeconds === 1 ? 'second' : 'seconds'}`; + } + + return timeString.trim(); } function Row(data) { const { row } = data; const [open, setOpen] = React.useState(false); // const classes = useRowStyles(); + const twelve_hr = JSON.parse(localStorage.getItem('12hr')); const options = { day: "numeric", @@ -55,11 +58,10 @@ function Row(data) { hour: "numeric", minute: "numeric", second: "numeric", - hour12: hour_format, + hour12: twelve_hr, }; - return ( *': { borderBottom: 'unset' } }}> @@ -76,7 +78,8 @@ function Row(data) { {!row.SeriesName ? row.NowPlayingItemName : row.SeriesName+' - '+ row.NowPlayingItemName} {row.Client} {Intl.DateTimeFormat('en-UK', options).format(new Date(row.ActivityDateInserted))} - {formatTotalWatchTime(row.PlaybackDuration) || '0 minutes'} + {/* {formatTotalWatchTime(row.results && row.results.length>0 ? row.results.reduce((acc, items) => acc +parseInt(items.PlaybackDuration),0): row.PlaybackDuration) || '0 minutes'} */} + {formatTotalWatchTime(row.PlaybackDuration) || '0 seconds'} {row.results.length !==0 ? row.results.length : 1} @@ -87,7 +90,7 @@ function Row(data) { - Username + User Title Client Date @@ -96,14 +99,14 @@ function Row(data) { - {row.results.map((resultRow) => ( + {row.results.sort((a, b) => new Date(b.ActivityDateInserted) - new Date(a.ActivityDateInserted)).map((resultRow) => ( {resultRow.UserName} {!resultRow.SeriesName ? resultRow.NowPlayingItemName : resultRow.SeriesName+' - '+ resultRow.NowPlayingItemName} {resultRow.Client} {Intl.DateTimeFormat('en-UK', options).format(new Date(resultRow.ActivityDateInserted))} - {formatTotalWatchTime(resultRow.PlaybackDuration) || '0 minutes'} + {formatTotalWatchTime(resultRow.PlaybackDuration) || '0 seconds'} 1 ))} @@ -117,10 +120,90 @@ function Row(data) { ); } +function EnhancedTableHead(props) { + const { order, orderBy, onRequestSort } = + props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + const headCells = [ + { + id: 'UserName', + numeric: false, + disablePadding: true, + label: 'Last User', + }, + { + id: 'NowPlayingItemName', + numeric: false, + disablePadding: false, + label: 'Title', + }, + { + id: 'Client', + numeric: false, + disablePadding: false, + label: 'Last Client', + }, + { + id: 'ActivityDateInserted', + numeric: false, + disablePadding: false, + label: 'Date', + }, + { + id: 'PlaybackDuration', + numeric: false, + disablePadding: false, + label: 'Total Playback', + }, + { + id: 'TotalPlays', + numeric: false, + disablePadding: false, + label: 'TotalPlays', + }, + ]; + + + return ( + + + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === 'desc' ? 'sorted descending' : 'sorted ascending'} + + ) : null} + + + ))} + + + ); +} + export default function ActivityTable(props) { const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); + const [order, setOrder] = React.useState('desc'); + const [orderBy, setOrderBy] = React.useState('ActivityDateInserted'); + if(rowsPerPage!==props.itemCount) { @@ -137,25 +220,70 @@ export default function ActivityTable(props) { setPage((prevPage) => prevPage - 1); }; + + function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; + } + + function getComparator(order, orderBy) { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); + } + + + function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + + stabilizedThis.sort((a, b) => { + + const order = comparator(a[0], b[0]); + if (order !== 0) { + return order; + } + return a[1] - b[1]; + + }); + + return stabilizedThis.map((el) => el[0]); + } + + const visibleRows = React.useMemo( + () => + stableSort(props.data, getComparator(order, orderBy)).slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage, + ), + [order, orderBy, page, rowsPerPage, getComparator, props.data], + ); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + + + return ( <>
- - - - Username - Title - Client - Date - Playback Duration - Plays - - + - {props.data - .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row) => ( + {visibleRows.map((row) => ( ))} {props.data.length===0 ? :''} diff --git a/src/pages/components/general/last-watched-card.js b/src/pages/components/general/last-watched-card.js index 9aeffb2..b288cfe 100644 --- a/src/pages/components/general/last-watched-card.js +++ b/src/pages/components/general/last-watched-card.js @@ -35,7 +35,7 @@ function LastWatchedCard(props) {
- {loaded ? null : } + {loaded ? null : } { @@ -11,36 +13,44 @@ export default function Navbar() { window.location.reload(); }; - const location = useLocation(); // use the useLocation hook from react-router-dom + const location = useLocation(); return ( - - - Jellystat - - - - - + +
+ + + + Jellystat + + + + + + +
+ +
); } diff --git a/src/pages/components/general/version-card.js b/src/pages/components/general/version-card.js new file mode 100644 index 0000000..8168ac2 --- /dev/null +++ b/src/pages/components/general/version-card.js @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Row from 'react-bootstrap/Row'; +import Col from 'react-bootstrap/Col'; + +import "../../css/settings/version.css"; +import { Card } from "react-bootstrap"; + +export default function VersionCard() { + + const token = localStorage.getItem('token'); + const [data, setData] = useState(); + useEffect(() => { + + const fetchVersion = () => { + if (token) { + const url = `/api/CheckForUpdates`; + + axios + .get(url, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + if(!data) + { + fetchVersion(); + } + + const intervalId = setInterval(fetchVersion, 60000 * 5); + return () => clearInterval(intervalId); + }, [data,token]); + + + if(!data) + { + return <>; + } + + + return ( + + + +
Jellystat {data.current_version} + + + + {data.update_available? + + New version available: {data.latest_version} + + : + <> + } + + + + ); + + +} diff --git a/src/pages/components/item-info.js b/src/pages/components/item-info.js index c5fa95b..a21e210 100644 --- a/src/pages/components/item-info.js +++ b/src/pages/components/item-info.js @@ -117,18 +117,30 @@ if(data && data.notfound) return ; } +const cardStyle = { + backgroundImage: `url(/Proxy/Items/Images/Backdrop?id=${(["Episode","Season"].includes(data.Type)? data.SeriesId : data.Id)}&fillWidth=800&quality=90)`, + height:'100%', + backgroundSize: 'cover', +}; + +const cardBgStyle = { + backgroundColor: 'rgb(0, 0, 0, 0.8)', + +}; + + return (
-
- -
- {data.PrimaryImageHash && !loaded ? : null} +
+ +
+ {data.PrimaryImageHash && !loaded ? : null}
- {(props.data.ImageBlurHashes || props.data.PrimaryImageHash )&& !loaded ? : null} + {(props.data.ImageBlurHashes || props.data.PrimaryImageHash )&& !loaded ? : null} {fallback ? - + diff --git a/src/pages/components/library/RecentlyAdded/recently-added-card.js b/src/pages/components/library/RecentlyAdded/recently-added-card.js index 2a6fd1d..10670e8 100644 --- a/src/pages/components/library/RecentlyAdded/recently-added-card.js +++ b/src/pages/components/library/RecentlyAdded/recently-added-card.js @@ -12,7 +12,7 @@ function RecentlyAddedCard(props) {
- {loaded ? null : } + {loaded ? null : } +
+

Media

- setSearchQuery(e.target.value)} className="my-3 w-25" /> + setSearchQuery(e.target.value)} className="my-3 w-sm-100 w-md-75 w-lg-25" />
diff --git a/src/pages/components/library/recently-added.js b/src/pages/components/library/recently-added.js index 2376b04..714f893 100644 --- a/src/pages/components/library/recently-added.js +++ b/src/pages/components/library/recently-added.js @@ -6,7 +6,7 @@ import RecentlyAddedCard from "./RecentlyAdded/recently-added-card"; import Config from "../../../lib/config"; import "../../css/users/user-details.css"; -function RecentlyPlayed(props) { +function RecentlyAdded(props) { const [data, setData] = useState(); const [config, setConfig] = useState(); @@ -22,33 +22,23 @@ function RecentlyPlayed(props) { } }; - // const fetchAdmin = async () => { - // try { - // let url=`/api/getAdminUsers`; - // const adminData = await axios.get(url, { - // headers: { - // Authorization: `Bearer ${config.token}`, - // "Content-Type": "application/json", - // }, - // }); - // return adminData.data[0].Id; - // // setData(itemData.data); - // } catch (error) { - // console.log(error); - // } - // }; + const fetchData = async () => { try { - // let adminId=await fetchAdmin(); - let url=`/stats/getRecentlyAdded?libraryid=${props.LibraryId}`; + let url=`/stats/getRecentlyAdded`; + if(props.LibraryId) + { + url+=`?libraryid=${props.LibraryId}`; + } + const itemData = await axios.get(url, { headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json", }, }); - setData(itemData.data); + setData(itemData.data.filter((item) => ["Series", "Movie","Audio"].includes(item.Type))); } catch (error) { console.log(error); } @@ -75,7 +65,7 @@ function RecentlyPlayed(props) {

Recently Added

- {data.filter((item) => ["Series", "Movie","Audio"].includes(item.Type)).map((item) => ( + {data && data.map((item) => ( ))} @@ -85,4 +75,4 @@ function RecentlyPlayed(props) { ); } -export default RecentlyPlayed; +export default RecentlyAdded; diff --git a/src/pages/components/libraryOverview.js b/src/pages/components/libraryOverview.js index 7661be4..9d05e19 100644 --- a/src/pages/components/libraryOverview.js +++ b/src/pages/components/libraryOverview.js @@ -11,8 +11,8 @@ import FilmLineIcon from "remixicon-react/FilmLineIcon"; export default function LibraryOverView() { const token = localStorage.getItem('token'); - const SeriesIcon= ; - const MovieIcon= ; + const SeriesIcon= ; + const MovieIcon= ; const [data, setData] = useState(); @@ -41,7 +41,7 @@ export default function LibraryOverView() { return (
-

Library Statistics

+

Library Overview

stat.CollectionType === "movies")} heading={"MOVIE LIBRARIES"} units={"MOVIES"} icon={MovieIcon}/> diff --git a/src/pages/components/libraryStatCard/library-stat-component.js b/src/pages/components/libraryStatCard/library-stat-component.js index afc0e71..3946d23 100644 --- a/src/pages/components/libraryStatCard/library-stat-component.js +++ b/src/pages/components/libraryStatCard/library-stat-component.js @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { Row, Col, Card } from "react-bootstrap"; function LibraryStatComponent(props) { @@ -48,7 +49,7 @@ function LibraryStatComponent(props) {
{index + 1} - {item.Name} + {item.Name}
diff --git a/src/pages/components/sessions/session-card.js b/src/pages/components/sessions/session-card.js index 3340d59..b886219 100644 --- a/src/pages/components/sessions/session-card.js +++ b/src/pages/components/sessions/session-card.js @@ -67,17 +67,13 @@ function sessionCard(props) { props.data.session.DeviceName.toLowerCase().includes(item)) || "other") : ( clientData.find(item => props.data.session.Client.toLowerCase().includes(item)) || "other") - ) - + - ".svg" - } + )} alt="" /> @@ -126,6 +122,22 @@ function sessionCard(props) { + {props.data.session.NowPlayingItem.ParentIndexNumber ? + + + +
+ + {'S'+props.data.session.NowPlayingItem.ParentIndexNumber +' - E'+ props.data.session.NowPlayingItem.IndexNumber} + + + + : + <> + + } + + diff --git a/src/pages/components/sessions/sessions.js b/src/pages/components/sessions/sessions.js index 8018928..205b6fe 100644 --- a/src/pages/components/sessions/sessions.js +++ b/src/pages/components/sessions/sessions.js @@ -40,7 +40,7 @@ function Sessions() { }, }) .then((data) => { - setData(data.data); + setData(data.data.filter(row => row.NowPlayingItem !== undefined)); }) .catch((error) => { console.log(error); @@ -79,9 +79,8 @@ function Sessions() {

Sessions

- {data && + {data && data.length>0 && data - .filter(row => row.NowPlayingItem !== undefined) .sort((a, b) => a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0")) ) diff --git a/src/pages/components/settings/Tasks.js b/src/pages/components/settings/Tasks.js new file mode 100644 index 0000000..9858ec1 --- /dev/null +++ b/src/pages/components/settings/Tasks.js @@ -0,0 +1,108 @@ +import React, { useState } from "react"; +import axios from "axios"; +import Button from "react-bootstrap/Button"; + + +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; + + +import "../../css/settings/settings.css"; + +export default function Tasks() { + const [processing, setProcessing] = useState(false); + const token = localStorage.getItem('token'); + async function beginSync() { + + + setProcessing(true); + + await axios + .get("/sync/beingSync", { + 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 }; + } + + 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(); + console.log('Button clicked!'); + } + + return ( +
+

Tasks

+ + +
No Activity Found
+ + + Task + Type + + + + + + + Synchronize with Jellyfin + Import + + + + + Backup Jellystat + Process + + + + +
+ + +
+ ); + + +} diff --git a/src/pages/components/settings/TerminalComponent.js b/src/pages/components/settings/TerminalComponent.js index c217a2a..d5eef1a 100644 --- a/src/pages/components/settings/TerminalComponent.js +++ b/src/pages/components/settings/TerminalComponent.js @@ -1,30 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import '../../css/websocket/websocket.css'; -const TerminalComponent = () => { - const [messages, setMessages] = useState([]); +function TerminalComponent(props){ + const [messages] = useState(props.data); - useEffect(() => { - try{ - - const socket = new WebSocket(`ws://127.0.0.1:${process.env.WS_PORT || 3004}`); - - // handle incoming messages - socket.addEventListener('message', (event) => { - let message = JSON.parse(event.data); - setMessages(message); - }); - - return () => { - socket.close(); - } - - }catch(error) - { - // console.log(error); - } - - }, []); return (
diff --git a/src/pages/components/settings/backupfiles.js b/src/pages/components/settings/backupfiles.js index 3738808..9eb0a8e 100644 --- a/src/pages/components/settings/backupfiles.js +++ b/src/pages/components/settings/backupfiles.js @@ -111,6 +111,8 @@ function Row(file) { + const twelve_hr = JSON.parse(localStorage.getItem('12hr')); + const options = { day: "numeric", month: "numeric", @@ -118,10 +120,11 @@ function Row(file) { hour: "numeric", minute: "numeric", second: "numeric", - hour12: false, + hour12: twelve_hr, }; + return ( *': { borderBottom: 'unset' } }}> diff --git a/src/pages/components/settings/librarySync.js b/src/pages/components/settings/librarySync.js deleted file mode 100644 index 3c84f25..0000000 --- a/src/pages/components/settings/librarySync.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState } from "react"; -import axios from "axios"; -import Button from "react-bootstrap/Button"; -import Form from 'react-bootstrap/Form'; -import Row from 'react-bootstrap/Row'; -import Col from 'react-bootstrap/Col'; - - -import "../../css/settings/settings.css"; - -export default function LibrarySync() { - const [processing, setProcessing] = useState(false); - const token = localStorage.getItem('token'); - async function beginSync() { - - - setProcessing(true); - - await axios - .get("/sync/beingSync", { - 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 }; - } - - 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(); - console.log('Button clicked!'); - } - - return ( -
-

Tasks

- - - - Synchronize with Jellyfin - - - - - - - - - - - Create Backup - - - - - - - -
- ); - - -} diff --git a/src/pages/components/settings/logs.js b/src/pages/components/settings/logs.js new file mode 100644 index 0000000..1d03a36 --- /dev/null +++ b/src/pages/components/settings/logs.js @@ -0,0 +1,210 @@ +import React, { useEffect } from "react"; +import axios from "axios"; +import {ButtonGroup, Button } from 'react-bootstrap'; + +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Collapse from '@mui/material/Collapse'; +import IconButton from '@mui/material/IconButton'; +import Box from '@mui/material/Box'; + +import AddCircleFillIcon from 'remixicon-react/AddCircleFillIcon'; +import IndeterminateCircleFillIcon from 'remixicon-react/IndeterminateCircleFillIcon'; + + + +import "../../css/settings/backups.css"; + +import TerminalComponent from "./TerminalComponent"; + +const token = localStorage.getItem('token'); + + +function Row(logs) { + const { data } = logs; + const [open, setOpen] = React.useState(false); + + + const twelve_hr = JSON.parse(localStorage.getItem('12hr')); + + const options = { + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: twelve_hr, + }; + + + +function formatDurationTime(seconds) { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + let timeString = ''; + + if (hours > 0) { + timeString += `${hours} ${hours === 1 ? 'hr' : 'hrs'} `; + } + + if (minutes > 0) { + timeString += `${minutes} ${minutes === 1 ? 'min' : 'mins'} `; + } + + if (remainingSeconds > 0) { + timeString += `${remainingSeconds} ${remainingSeconds === 1 ? 'second' : 'seconds'}`; + } + + return timeString.trim(); +} + + + return ( + + *': { borderBottom: 'unset' } }}> + + {if(data.Log.length>1){setOpen(!open);}}} + > + {!open ? 1 ?1 : 0} cursor={data.Log.length>1 ? "pointer":"default"}/> : } + + + {data.Name} + {data.Type} + {Intl.DateTimeFormat('en-UK', options).format(new Date(data.TimeRun))} + {formatDurationTime(data.Duration)} + {data.ExecutionType} +
{data.Result}
+ +
+ + + + + + + + + + + + +
+
+
+
+
+
+ ); +} + + +export default function Logs() { + + const [data, setData]=React.useState([]); + const [rowsPerPage] = React.useState(10); + const [page, setPage] = React.useState(0); + + + + + +useEffect(() => { + const fetchData = async () => { + try { + const logs = await axios.get(`/logs/getLogs`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + setData(logs.data); + } catch (error) { + console.log(error); + } + }; + + fetchData(); + + const intervalId = setInterval(fetchData, 60000 * 5); + return () => clearInterval(intervalId); + }, [data]); + + +const handleNextPageClick = () => { + setPage((prevPage) => prevPage + 1); +}; + +const handlePreviousPageClick = () => { + setPage((prevPage) => prevPage - 1); +}; + + + + return ( +
+

Logs

+ + + + + + + Name + Type + Date Created + Duration + Execution Type + Result + + + + {data && data.sort((a, b) =>new Date(b.TimeRun) - new Date(a.TimeRun)).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((log,index) => ( + + ))} + {data.length===0 ? :''} + + + +
No Logs Found
+
+ +
+ + + + + + + + +
{`${page *rowsPerPage + 1}-${Math.min((page * rowsPerPage+ 1 ) + (rowsPerPage - 1),data.length)} of ${data.length}`}
+ + + + +
+
+
+ ); + + +} \ No newline at end of file diff --git a/src/pages/components/settings/settingsConfig.js b/src/pages/components/settings/settingsConfig.js index 7055dc1..9d24e97 100644 --- a/src/pages/components/settings/settingsConfig.js +++ b/src/pages/components/settings/settingsConfig.js @@ -7,6 +7,8 @@ import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Button from 'react-bootstrap/Button'; import Alert from 'react-bootstrap/Alert'; +import ToggleButton from 'react-bootstrap/ToggleButton'; +import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup'; @@ -21,6 +23,17 @@ export default function SettingsConfig() { const [loadSate, setloadSate] = useState("Loading"); const [submissionMessage, setsubmissionMessage] = useState(""); const token = localStorage.getItem('token'); + const [twelve_hr, set12hr] = useState(localStorage.getItem('12hr') === 'true'); + + const storage_12hr = localStorage.getItem('12hr'); + + if(storage_12hr===null) + { + localStorage.setItem('12hr',false); + set12hr(false); + }else if(twelve_hr===null){ + set12hr(Boolean(storage_12hr)); + } useEffect(() => { Config() @@ -51,7 +64,7 @@ export default function SettingsConfig() { }, }) .catch((error) => { - let errorMessage= `Error : ${error}`; + // let errorMessage= `Error : ${error}`; }); let data=result.data; @@ -102,28 +115,34 @@ export default function SettingsConfig() { return
{submissionMessage}
; } + + function toggle12Hr(is_12_hr){ + set12hr(is_12_hr); + localStorage.setItem('12hr',is_12_hr); + }; + return (
-

General Settings

+

Settings

- + Jellyfin Url - + - + API Key - + {isSubmitted !== "" ? ( @@ -139,14 +158,31 @@ export default function SettingsConfig() { ) : ( <> )} -
+
+ +
+ + Hour Format + + + {toggle12Hr(true);}}>12 Hours + {toggle12Hr(false);}}>24 Hours + + + + +
+ + + +
); diff --git a/src/pages/components/statCards/ItemStatComponent.js b/src/pages/components/statCards/ItemStatComponent.js index 008f12c..bffcfb8 100644 --- a/src/pages/components/statCards/ItemStatComponent.js +++ b/src/pages/components/statCards/ItemStatComponent.js @@ -45,12 +45,12 @@ function ItemStatComponent(props) { <> {!loaded && (
- +
)} setLoaded(false)} diff --git a/src/pages/components/statCards/most_active_users.js b/src/pages/components/statCards/most_active_users.js index e8109d3..98dc300 100644 --- a/src/pages/components/statCards/most_active_users.js +++ b/src/pages/components/statCards/most_active_users.js @@ -10,6 +10,7 @@ function MostActiveUsers(props) { const [data, setData] = useState(); const [days, setDays] = useState(30); const [config, setConfig] = useState(null); + const [loaded, setLoaded]= useState(true); useEffect(() => { @@ -68,9 +69,19 @@ function MostActiveUsers(props) { return <>; } + const UserImage = () => { + return ( + setLoaded(false)} + /> + ); + }; return ( - } data={data} heading={"MOST ACTIVE USERS"} units={"Plays"}/> + : } data={data} heading={"MOST ACTIVE USERS"} units={"Plays"}/> ); } diff --git a/src/pages/css/about.css b/src/pages/css/about.css new file mode 100644 index 0000000..0786f69 --- /dev/null +++ b/src/pages/css/about.css @@ -0,0 +1,20 @@ +@import './variables.module.css'; +.about +{ + background-color: var(--second-background-color) !important; + border-color: transparent !important; + color: white !important; + + +} + +.about a +{ + text-decoration: none; +} + +.about a:hover +{ + text-decoration: underline; +} + diff --git a/src/pages/css/activity/activity-table.css b/src/pages/css/activity/activity-table.css index 068d891..04e4ab2 100644 --- a/src/pages/css/activity/activity-table.css +++ b/src/pages/css/activity/activity-table.css @@ -10,7 +10,7 @@ td,th, td>button { color: white !important; - background-color: var(--secondary-background-color); + background-color: var(--tertiary-background-color); } @@ -89,4 +89,14 @@ font-size: 1em; { background-color: var(--primary-color) !important; border-color: var(--primary-color)!important; +} + +.MuiTableCell-head > .Mui-active, .MuiTableSortLabel-icon +{ + color: var(--secondary-color) !important; +} + +.MuiTableCell-head :hover +{ + color: var(--secondary-color) !important; } \ No newline at end of file diff --git a/src/pages/css/home.css b/src/pages/css/home.css index 6b963c6..2eadae4 100644 --- a/src/pages/css/home.css +++ b/src/pages/css/home.css @@ -1,4 +1,5 @@ .Home { color: white; + margin-bottom: 20px; } \ No newline at end of file diff --git a/src/pages/css/items/item-details.css b/src/pages/css/items/item-details.css index 9186acb..7b302da 100644 --- a/src/pages/css/items/item-details.css +++ b/src/pages/css/items/item-details.css @@ -3,9 +3,12 @@ { color:white; background-color: var(--secondary-background-color); - padding: 20px; margin: 20px 0; - border-radius: 8px; +} + +.item-banner-image +{ + margin-right: 20px; } .item-name diff --git a/src/pages/css/lastplayed.css b/src/pages/css/lastplayed.css index 4c3168a..f2d1186 100644 --- a/src/pages/css/lastplayed.css +++ b/src/pages/css/lastplayed.css @@ -2,13 +2,14 @@ .last-played-container { display: flex; - overflow-x: auto; + overflow-x: scroll; background-color: var(--secondary-background-color); padding: 20px; border-radius: 8px; color: white; margin-bottom: 20px; min-height: 300px; + } .last-played-container::-webkit-scrollbar { diff --git a/src/pages/css/library/libraries.css b/src/pages/css/library/libraries.css index ec0c8db..0bff990 100644 --- a/src/pages/css/library/libraries.css +++ b/src/pages/css/library/libraries.css @@ -2,7 +2,9 @@ { color: white; display: grid; - grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 20px; + } diff --git a/src/pages/css/library/library-card.css b/src/pages/css/library/library-card.css index 07b557c..25579e2 100644 --- a/src/pages/css/library/library-card.css +++ b/src/pages/css/library/library-card.css @@ -2,7 +2,7 @@ .lib-card{ color: white; - max-width: 400px; + /* max-width: 400px; */ } @@ -20,6 +20,7 @@ .library-card-image { max-height: 170px; + overflow: hidden; } @@ -30,7 +31,7 @@ background-repeat: no-repeat; background-size: cover; transition: all 0.2s ease-in-out; - + max-height: 170px; } diff --git a/src/pages/css/library/media-items.css b/src/pages/css/library/media-items.css index 28ae0f3..5243a22 100644 --- a/src/pages/css/library/media-items.css +++ b/src/pages/css/library/media-items.css @@ -33,7 +33,7 @@ background-color: #88888883; /* set thumb color */ } - .form-control + .library-items > div> .form-control { color: white !important; background-color: var(--secondary-background-color) !important; @@ -41,7 +41,7 @@ } - .form-control:focus + .library-items > div> .form-control:focus { box-shadow: none !important; border-color: var(--primary-color) !important; diff --git a/src/pages/css/libraryOverview.css b/src/pages/css/libraryOverview.css index 484a420..11192c4 100644 --- a/src/pages/css/libraryOverview.css +++ b/src/pages/css/libraryOverview.css @@ -4,8 +4,9 @@ grid-template-columns: repeat(auto-fit, minmax(320px, 520px)); grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ + background-color: var(--secondary-background-color); border-radius: 8px; - /* margin-right: 20px; */ + padding: 20px; } .library-stat-card @@ -84,12 +85,8 @@ .library-banner-image { - - height: 180px; - width: 120px; - - + width: 120px; } diff --git a/src/pages/css/loading.css b/src/pages/css/loading.css index 8044d1f..0b6c992 100644 --- a/src/pages/css/loading.css +++ b/src/pages/css/loading.css @@ -1,3 +1,4 @@ +@import './variables.module.css'; .loading { margin: 0px; @@ -6,8 +7,7 @@ display: flex; justify-content: center; align-items: center; - /* z-index: 9999; */ - background-color: #1e1c22; + background-color: var(--background-color); transition: opacity 800ms ease-in; opacity: 1; } diff --git a/src/pages/css/navbar.css b/src/pages/css/navbar.css index 80150cf..7e29661 100644 --- a/src/pages/css/navbar.css +++ b/src/pages/css/navbar.css @@ -1,37 +1,85 @@ @import './variables.module.css'; .navbar { - background-color: var(--primary-color); - + background-color: var(--secondary-background-color); + border-right: 1px solid #414141 !important; } +@media (min-width: 768px) { + .navbar { + min-height: 100vh; + border-bottom: 1px solid #414141 !important; + + } +} + +.navbar .navbar-brand{ + margin-top: 20px; + font-size: 32px; + font-weight: 500; +} + +.navbar .navbar-nav{ + width: 100%; + margin-top: 20px; +} + +.logout +{ + + color: var(--secondary-color) !important; +} +.navbar-toggler > .collapsed +{ + right: 0; +} +/* .navbar-toggler-icon +{ + width: 100% !important; +} */ + + .navitem { - display: flex; - align-items: center; - height: 100%; + color: white; - font-size: 16px; + font-size: 18px !important; text-decoration: none; - padding: 0 20px; + margin-right: 10px; + background-color: var(--background-color); + transition: all 0.4s ease-in-out; + border-radius: 8px; + margin-bottom: 10px; + width: 90%; +} + +@media (min-width: 768px) { + .navitem { + border-radius: 0 8px 8px 0; + } +} + +.navitem:hover { + background-color: var(--primary-color); +} + +.active +{ + background-color: var(--primary-color); transition: background-color 0.2s ease-in-out; } +.nav-link +{ + display: flex !important; +} + + .nav-text { margin-left: 10px; } -.active -{ - /* background-color: #308df046 !important; */ - background-color: rgba(0,0,0,0.6); - transition: background-color 0.2s ease-in-out; -} -.navitem:hover { - /* background-color: #326aa541; */ - background-color: rgba(0,0,0,0.4); -} diff --git a/src/pages/css/sessions.css b/src/pages/css/sessions.css index e2e27eb..ceaa370 100644 --- a/src/pages/css/sessions.css +++ b/src/pages/css/sessions.css @@ -4,6 +4,9 @@ display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 520px)); grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ + background-color: var(--secondary-background-color); + border-radius: 8px; + padding: 20px; } .session-card { diff --git a/src/pages/css/settings/backups.css b/src/pages/css/settings/backups.css index 4ddb2d7..beb7964 100644 --- a/src/pages/css/settings/backups.css +++ b/src/pages/css/settings/backups.css @@ -31,4 +31,4 @@ td{ .upload-file:focus { box-shadow: none !important; -} \ No newline at end of file +} diff --git a/src/pages/css/settings/settings.css b/src/pages/css/settings/settings.css index f20be35..6088abd 100644 --- a/src/pages/css/settings/settings.css +++ b/src/pages/css/settings/settings.css @@ -14,7 +14,7 @@ .tasks { color: white; - margin-inline: 10px; + /* margin-inline: 10px; */ } @@ -56,4 +56,19 @@ margin-bottom: 5px; } - \ No newline at end of file + + + + .settings-form > div> div> .form-control + { + color: white !important; + background-color: var(--background-color) !important; + border-color: var(--background-color) !important; + } + + + .settings-form > div> div> .form-control:focus + { + box-shadow: none !important; + border-color: var(--primary-color) !important; + } \ No newline at end of file diff --git a/src/pages/css/settings/version.css b/src/pages/css/settings/version.css new file mode 100644 index 0000000..3d9787e --- /dev/null +++ b/src/pages/css/settings/version.css @@ -0,0 +1,46 @@ +@import '../variables.module.css'; +.version +{ + background-color: var(--background-color) !important; + + color: white !important; + position: fixed !important; + bottom: 0; + max-width: 200px; + text-align: center; + width: 100%; + + +} + + +.version a +{ + text-decoration: none; +} + +.version a:hover +{ + text-decoration: underline; +} + +.nav-pills > .nav-item , .nav-pills > .nav-item > .nav-link +{ + color: white !important; +} + +.nav-pills > .nav-item .active +{ + background-color: var(--primary-color) !important; + color: white !important; +} + +.nav-pills > .nav-item :hover +{ + background-color: var(--primary-color) !important; +} + +.nav-pills > .nav-item .active> .nav-link +{ + color: white; +} \ No newline at end of file diff --git a/src/pages/css/setup.css b/src/pages/css/setup.css index f2255d5..db1ffcb 100644 --- a/src/pages/css/setup.css +++ b/src/pages/css/setup.css @@ -51,8 +51,8 @@ h2{ pointer-events: none; transition: .2s; } -input:focus ~ label, -input:valid ~ label{ +.form-box> form> .inputbox> input:focus ~ label, +.form-box> form> .inputbox> input:valid ~ label{ top: -15px; } .inputbox input { diff --git a/src/pages/css/statCard.css b/src/pages/css/statCard.css index 4ed6e63..5921362 100644 --- a/src/pages/css/statCard.css +++ b/src/pages/css/statCard.css @@ -5,6 +5,9 @@ grid-template-columns: repeat(auto-fit, minmax(320px, 520px)); grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ margin-top: 8px; + background-color: var(--secondary-background-color); + border-radius: 8px; + padding: 20px; } .stat-card{ border: 0 !important; @@ -21,6 +24,8 @@ + + .stat-card-image { width: 120px !important; height: 180px; @@ -29,7 +34,12 @@ .stat-card-icon { width: 120px !important; - height: 180px; + + + position: relative; + top: 50%; + left: 65%; + transform: translate(-50%, -50%); } diff --git a/src/pages/css/users/user-details.css b/src/pages/css/users/user-details.css index 6fdddac..3439add 100644 --- a/src/pages/css/users/user-details.css +++ b/src/pages/css/users/user-details.css @@ -1,4 +1,5 @@ @import '../variables.module.css'; + .user-detail-container { color:white; diff --git a/src/pages/css/variables.module.css b/src/pages/css/variables.module.css index e9a8739..4b9c8ec 100644 --- a/src/pages/css/variables.module.css +++ b/src/pages/css/variables.module.css @@ -2,5 +2,6 @@ --primary-color: #5a2da5; --secondary-color: #00A4DC; --background-color: #1e1c22; - --secondary-background-color: rgba(100, 100, 100,0.2); + --secondary-background-color: #2c2a2f; + --tertiary-background-color: #2f2e31; } \ No newline at end of file diff --git a/src/pages/css/websocket/websocket.css b/src/pages/css/websocket/websocket.css index c4a9001..0f4181f 100644 --- a/src/pages/css/websocket/websocket.css +++ b/src/pages/css/websocket/websocket.css @@ -10,6 +10,7 @@ .console-message { margin-bottom: 10px; + font-size: 1.1rem; } .console-text { diff --git a/src/pages/css/width_breakpoint_css.css b/src/pages/css/width_breakpoint_css.css new file mode 100644 index 0000000..8fbf6d0 --- /dev/null +++ b/src/pages/css/width_breakpoint_css.css @@ -0,0 +1,142 @@ + +/*sourced from https://drive.google.com/uc?export=view&id=1yTLwNiCZhIdCWolQldwq4spHQkgZDqkG */ +/* Small devices (landscape phones, 576px and up)*/ +@media (min-width: 576px) { + .w-sm-100 { + width: 100% !important; + } + + .w-sm-75 { + width: 75% !important; + } + + .w-sm-50 { + width: 50% !important; + } + + .w-sm-25 { + width: 25% !important; + } + + .h-sm-100 { + height: 100% !important; + } + + .h-sm-75 { + height: 75% !important; + } + + .h-sm-50 { + height: 50% !important; + } + + .h-sm-25 { + height: 25% !important; + } +} + + +/* Medium devices (tablets, 768px and up)*/ +@media (min-width: 768px) { + .w-md-100 { + width: 100% !important; + } + + .w-md-75 { + width: 75% !important; + } + + .w-md-50 { + width: 50% !important; + } + + .w-md-25 { + width: 25% !important; + } + + .h-md-100 { + height: 100% !important; + } + + .h-md-75 { + height: 75% !important; + } + + .h-md-50 { + height: 50% !important; + } + + .h-md-25 { + height: 25% !important; + } +} + +/* Large devices (desktops, 992px and up)*/ +@media (min-width: 992px) { + .w-lg-100 { + width: 100% !important; + } + + .w-lg-75 { + width: 75% !important; + } + + .w-lg-50 { + width: 50% !important; + } + + .w-lg-25 { + width: 25% !important; + } + + .h-lg-100 { + height: 100% !important; + } + + .h-lg-75 { + height: 75% !important; + } + + .h-lg-50 { + height: 50% !important; + } + + .h-lg-25 { + height: 25% !important; + } +} + +/* Extra large devices (large desktops, 1200px and up)*/ +@media (min-width: 1200px) { + .w-xl-100 { + width: 100% !important; + } + + .w-xl-75 { + width: 75% !important; + } + + .w-xl-50 { + width: 50% !important; + } + + .w-xl-25 { + width: 25% !important; + } + + .h-xl-100 { + height: 100% !important; + } + + .h-xl-75 { + height: 75% !important; + } + + .h-xl-50 { + height: 50% !important; + } + + .h-xl-25 { + height: 25% !important; + } +} \ No newline at end of file diff --git a/src/pages/home.js b/src/pages/home.js index 42801f2..4f63304 100644 --- a/src/pages/home.js +++ b/src/pages/home.js @@ -5,13 +5,14 @@ import './css/home.css' import Sessions from './components/sessions/sessions' import HomeStatisticCards from './components/HomeStatisticCards' import LibraryOverView from './components/libraryOverview' - +import RecentlyAdded from './components/library/recently-added' export default function Home() { return ( -
+
+ diff --git a/src/pages/libraries.js b/src/pages/libraries.js index c9c3202..b736167 100644 --- a/src/pages/libraries.js +++ b/src/pages/libraries.js @@ -7,7 +7,7 @@ import "./css/library/libraries.css"; import Loading from "./components/general/loading"; import LibraryCard from "./components/library/library-card"; -import Row from "react-bootstrap/Row"; +import Container from "react-bootstrap/Container"; @@ -82,7 +82,7 @@ function Libraries() {

Libraries

- +
{data && data.map((item) => ( @@ -90,7 +90,7 @@ function Libraries() { ))} - +
); diff --git a/src/pages/settings.js b/src/pages/settings.js index d455e45..0dd8b16 100644 --- a/src/pages/settings.js +++ b/src/pages/settings.js @@ -2,11 +2,12 @@ import React from "react"; import {Tabs, Tab } from 'react-bootstrap'; import SettingsConfig from "./components/settings/settingsConfig"; -import LibrarySync from "./components/settings/librarySync"; - +import Tasks from "./components/settings/Tasks"; import BackupFiles from "./components/settings/backupfiles"; -import TerminalComponent from "./components/settings/TerminalComponent"; +import Logs from "./components/settings/logs"; + +// import TerminalComponent from "./components/settings/TerminalComponent"; @@ -18,19 +19,28 @@ export default function Settings() { return (
- - - - - + + + + + + - + + + + + + + + + - + {/* */}
); diff --git a/src/pages/testing.js b/src/pages/testing.js index 578a0ca..9a41a99 100644 --- a/src/pages/testing.js +++ b/src/pages/testing.js @@ -9,7 +9,7 @@ import './css/library/libraries.css'; // import LibraryOverView from './components/libraryOverview'; // import HomeStatisticCards from './components/HomeStatisticCards'; // import Sessions from './components/sessions/sessions'; -import DailyPlayStats from './components/statistics/daily-play-count'; +import MostActiveUsers from './components/statCards/most_active_users'; @@ -52,7 +52,7 @@ function Testing() { return (
- +
diff --git a/src/pages/users.js b/src/pages/users.js index b196a56..c591817 100644 --- a/src/pages/users.js +++ b/src/pages/users.js @@ -11,6 +11,10 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; +import TableSortLabel from '@mui/material/TableSortLabel'; +import Box from '@mui/material/Box'; +import { visuallyHidden } from '@mui/utils'; + import "./css/users/users.css"; @@ -18,6 +22,85 @@ import Loading from "./components/general/loading"; const token = localStorage.getItem('token'); + +function EnhancedTableHead(props) { + const { order, orderBy, onRequestSort } = + props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + const headCells = [ + { + id: 'UserName', + numeric: false, + disablePadding: true, + label: 'User', + }, + { + id: 'LastWatched', + numeric: false, + disablePadding: false, + label: 'Last Watched', + }, + { + id: 'LastClient', + numeric: false, + disablePadding: false, + label: 'Last Client', + }, + { + id: 'TotalPlays', + numeric: false, + disablePadding: false, + label: 'Plays', + }, + { + id: 'TotalWatchTime', + numeric: false, + disablePadding: false, + label: 'Watch Time', + }, + { + id: 'LastSeen', + numeric: false, + disablePadding: false, + label: 'Last Seen', + }, + ]; + + + return ( + + + + {headCells.map((headCell) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === 'desc' ? 'sorted descending' : 'sorted ascending'} + + ) : null} + + + ))} + + + ); +} + + function Row(row) { const { data } = row; @@ -80,7 +163,7 @@ function Row(row) { {data.LastWatched || 'never'} {data.LastClient || 'n/a'} {data.TotalPlays} - {formatTotalWatchTime(data.TotalWatchTime) || 0} + {formatTotalWatchTime(data.TotalWatchTime) || '0 minutes'} {data.LastSeen ? formatLastSeenTime(data.LastSeen) : 'never'} @@ -95,6 +178,11 @@ function Users() { const [page, setPage] = React.useState(0); const [itemCount,setItemCount] = useState(10); + const [order, setOrder] = React.useState('asc'); + const [orderBy, setOrderBy] = React.useState('LastSeen'); + + + @@ -154,6 +242,101 @@ function Users() { setPage((prevPage) => prevPage - 1); }; + function formatLastSeenTime(time) { + if(!time) + { + return ' never'; + } + const units = { + days: ['Day', 'Days'], + hours: ['Hour', 'Hours'], + minutes: ['Minute', 'Minutes'], + seconds: ['Second', 'Seconds'] + }; + + let formattedTime = ''; + + for (const unit in units) { + if (time[unit]) { + const unitName = units[unit][time[unit] > 1 ? 1 : 0]; + formattedTime += `${time[unit]} ${unitName} `; + } + } + + return `${formattedTime}ago`; + } + + + + function descendingComparator(a, b, orderBy) { + if (orderBy==='LastSeen') { + let order_a=formatLastSeenTime(a[orderBy]); + let order_b=formatLastSeenTime(b[orderBy]); + if (order_b > order_a) { + return -1; + } + if (order_a< order_b) { + return 1; + } + return 0; + } + + if (orderBy === 'TotalPlays') { + let order_a = parseInt(a[orderBy]); + let order_b = parseInt(b[orderBy]); + + if (order_a < order_b) { + return -1; + } + if (order_a > order_b) { + return 1; + } + return 0; + } + + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; + } + + function getComparator(order, orderBy) { + return order === 'desc' + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); + } + + + function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + + stabilizedThis.sort((a, b) => { + + const order = comparator(a[0], b[0]); + if (order !== 0) { + return order; + } + return a[1] - b[1]; + + }); + + return stabilizedThis.map((el) => el[0]); + } + + const visibleRows = stableSort(data, getComparator(order, orderBy)).slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage, + ); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + @@ -174,20 +357,14 @@ function Users() { - - - - User - Last Watched - Last Client - Plays - Watch Time - Last Seen - - + - {data && data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((row) => ( + {visibleRows.map((row) => ( ))} {data.length===0 ? :''} diff --git a/src/setupProxy.js b/src/setupProxy.js index 6bdefa0..54c44b2 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -43,6 +43,13 @@ module.exports = function(app) { changeOrigin: true, }) ); + app.use( + `/logs`, + createProxyMiddleware({ + target: `http://127.0.0.1:${process.env.PORT || 3003}`, + changeOrigin: true, + }) + ); app.use( `/ws`, createProxyMiddleware({
No Users Found