From 28ed76d6c48963a08e9965e1cee2a5f834befc25 Mon Sep 17 00:00:00 2001 From: Thegan Govender Date: Sun, 12 Mar 2023 20:48:34 +0200 Subject: [PATCH] bck/frnt end changes to accomade sync and stats --- .vscode/launch.json | 11 +- backend/WebsocketHandler.js | 48 ++ backend/api.js | 433 +--------- backend/server.js | 4 + backend/stats.js | 18 + backend/sync.js | 778 ++++++++++++++++-- package-lock.json | 73 +- package.json | 3 +- src/classes/jellyfin-api.js | 49 +- src/pages/activity.js | 70 +- src/pages/components/libraryOverview.js | 114 +-- src/pages/components/recent-card.js | 117 +-- src/pages/components/recentlyplayed.js | 64 +- src/pages/components/session-card.js | 232 ++++-- src/pages/components/sessions.js | 58 +- .../settings/WebSocketComponent .js | 49 ++ src/pages/components/settings/librarySync.js | 40 + .../components/settings/settingsConfig.js | 153 ++++ src/pages/css/libraryOverview.css | 75 +- src/pages/css/websocket/websocket.css | 19 + src/pages/libraries.js | 155 ++-- src/pages/settings.js | 149 +--- src/pages/setup.js | 159 ++-- src/pages/useractivity.js | 240 +++--- 24 files changed, 1820 insertions(+), 1291 deletions(-) create mode 100644 backend/WebsocketHandler.js create mode 100644 backend/stats.js create mode 100644 src/pages/components/settings/WebSocketComponent .js create mode 100644 src/pages/components/settings/librarySync.js create mode 100644 src/pages/components/settings/settingsConfig.js create mode 100644 src/pages/css/websocket/websocket.css diff --git a/.vscode/launch.json b/.vscode/launch.json index 7312014..e468562 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,16 +1,19 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { "type": "chrome", "request": "launch", "name": "Launch Chrome against localhost", "url": "http://10.0.0.20:3000", "webRoot": "${workspaceFolder}" + }, + { + "type": "node-terminal", + "name": "Run Script: start", + "request": "launch", + "command": "npm run start", + "cwd": "${workspaceFolder}" } ] } \ No newline at end of file diff --git a/backend/WebsocketHandler.js b/backend/WebsocketHandler.js new file mode 100644 index 0000000..37ad4f9 --- /dev/null +++ b/backend/WebsocketHandler.js @@ -0,0 +1,48 @@ +const WebSocket = require('ws'); + +function createWebSocketServer(port) { + const wss = new WebSocket.Server({ port }); + let connected = false; + + // function to handle WebSocket connections + + function handleConnection(ws) { + if (!connected) { + console.log('Client connected'); + connected = true; + + // listen for messages from the client + ws.on('message', (message) => { + console.log(`Received message: ${message}`); + }); + + // listen for close events + ws.on('close', () => { + console.log('Client disconnected'); + connected = false; + });} + else + { + console.log('WebSocket connection already established'); + } + } + + + // 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) { + wss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + let parsedMessage = JSON.stringify(message); + console.log(parsedMessage); + client.send(parsedMessage); + } + }); + } + + return sendMessageToClients; +} + +module.exports = createWebSocketServer; diff --git a/backend/api.js b/backend/api.js index 75a727e..2301abd 100644 --- a/backend/api.js +++ b/backend/api.js @@ -1,42 +1,40 @@ // api.js -const express = require('express'); -const pgp = require('pg-promise')(); -const db = require('./db'); +const express = require("express"); +// const pgp = require("pg-promise")(); +const db = require("./db"); const router = express.Router(); -router.get('/test', async (req, res) => { +router.get("/test", async (req, res) => { console.log(`ENDPOINT CALLED: /test`); - res.send('Backend Responded Succesfully'); + res.send("Backend Responded Succesfully"); }); -router.get('/getconfig', async (req, res) => { +router.get("/getconfig", async (req, res) => { const { rows } = await db.query('SELECT * FROM app_config where "ID"=1'); console.log(`ENDPOINT CALLED: /getconfig: ` + rows); // console.log(`ENDPOINT CALLED: /setconfig: `+rows.length); res.send(rows); - }); -router.post('/setconfig', async (req, res) => { +router.post("/setconfig", async (req, res) => { const { JF_HOST, JF_API_KEY } = req.body; - const { rows } = await db.query('UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1', [JF_HOST, JF_API_KEY]); + const { rows } = await db.query( + 'UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1', + [JF_HOST, JF_API_KEY] + ); console.log({ JF_HOST: JF_HOST, JF_API_KEY: JF_API_KEY }); res.send(rows); - console.log(`ENDPOINT CALLED: /setconfig: `); - }); - -router.get('/getAllFromJellyfin', async (req, res) => { - - const sync = require('./sync'); +router.get("/getAllFromJellyfin", async (req, res) => { + const sync = require("./sync"); 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' }); + res.send({ error: "Config Details Not Found" }); return; } @@ -45,410 +43,7 @@ router.get('/getAllFromJellyfin', async (req, res) => { res.send(results); - - console.log(`ENDPOINT CALLED: /getAllFromJellyfin: `); - -}); -router.get('/getShows', async (req, res) => { - - const sync = require('./sync'); - 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' }); - return; - } - - const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); - const results = await _sync.getShows(); - - res.send(results); - - - - console.log(`ENDPOINT CALLED: /getShows: `); - -}); - -router.get('/writeAllShows', async (req, res) => { - - const sync = require('./sync'); - 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' }); - return; - } - - const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); - const data = await _sync.getShows(); - const existingIds = await db.query('SELECT "Id" FROM jf_library_shows').then(res => res.rows.map(row => row.Id)); - - - const columns = ['Id', 'Name', 'ServerId', 'PremiereDate', 'EndDate', 'CommunityRating', 'RunTimeTicks', 'ProductionYear', 'IsFolder', 'Type', 'Status', 'ImageTagsPrimary', 'ImageTagsBanner', 'ImageTagsLogo', 'ImageTagsThumb', 'BackdropImageTags', 'ParentId']; // specify the columns to insert into - - const dataToInsert = data.filter(row => !existingIds.includes(row.Id)).map(item => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - PremiereDate: item.PremiereDate, - EndDate: item.EndDate, - CommunityRating: item.CommunityRating, - RunTimeTicks: item.RunTimeTicks, - ProductionYear: item.ProductionYear, - IsFolder: item.IsFolder, - Type: item.Type, - Status: item.Status, - ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, - ImageTagsBanner: item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null, - ImageTagsLogo: item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null, - ImageTagsThumb: item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null, - BackdropImageTags: item.BackdropImageTags[0], - ParentId: item.ParentId - })); - - if(!dataToInsert || dataToInsert.length==0) - { - res.send(('No new shows to insert')); - return; - } - - - (async () => { - // const client = await pool.connect(); - - try { - await db.query('BEGIN'); - - const query = pgp.helpers.insert(dataToInsert, columns, 'jf_library_shows'); - await db.query(query); - - await db.query('COMMIT'); - console.log('Bulk insert successful'); - res.send('Bulk insert successful'); - } catch (error) { - await db.query('ROLLBACK'); - console.error('Error performing bulk insert:', error); - res.send(('Error performing bulk insert:', error)); - } - })(); - - - - console.log(`ENDPOINT CALLED: /getShows: `); - -}); - -router.get('/getSeasonsAndEpisodes', async (req, res) => { - - const sync = require('./sync'); - const results=[]; - 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' }); - return; - } - - const { rows:shows } = await db.query('SELECT * FROM jf_library_shows'); - const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); - - if (shows && shows.length > 0) { - for (const show of shows) { - const data = await _sync.getSeasonsAndEpisodes(show.Id); - results.push(data); - - } - - } else { - console.log("No shows found."); - results.push({Status:'Error',Message:'No shows found.'}); - - } - - - - - res.send(results); - - console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `); - -}); - - -router.get('/writeSeasonsAndEpisodes', async (req, res) => { - - const sync = require('./sync'); - const results=[]; - 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' }); - return; - } - - const { rows:shows } = await db.query('SELECT * FROM jf_library_shows'); - const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); - - if (shows && shows.length > 0) { - for (const show of shows) { - const data = await _sync.getSeasonsAndEpisodes(show.Id); - const columnSeasons = ['Id', 'Name', 'ServerId', 'IndexNumber', 'Type', 'ParentLogoItemId', 'ParentBackdropItemId', 'ParentBackdropImageTags', 'SeriesName', 'SeriesId', 'SeriesPrimaryImageTag']; // specify the columns to insert into - const columnEpisodes = ['Id', 'EpisodeId', 'Name', 'ServerId', 'PremiereDate', 'OfficialRating', 'CommunityRating', 'RunTimeTicks', 'ProductionYear', 'IndexNumber', 'ParentIndexNumber', 'Type', 'ParentLogoItemId', 'ParentBackdropItemId', 'ParentBackdropImageTags', 'SeriesId', 'SeasonId', 'SeasonName', 'SeriesName']; // specify the columns to insert into - - const existingSeasons = await db.query('SELECT "Id" FROM jf_library_seasons').then(res => res.rows.map(row => row.Id)); - const existingEpisodes = await db.query('SELECT "Id" FROM jf_library_episodes').then(res => res.rows.map(row => row.Id)); - - const seasonsToInsert = await data.allSeasons.filter(row => !existingSeasons.includes(row.Id)).map(item => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - IndexNumber: item.IndexNumber, - Type: item.Type, - ParentLogoItemId: item.ParentLogoItemId, - ParentBackdropItemId: item.ParentBackdropItemId, - ParentBackdropImageTags: item.ParentBackdropImageTags!==undefined ? item.ParentBackdropImageTags[0] : null, - SeriesName: item.SeriesName, - SeriesId: item.ParentId, - SeriesPrimaryImageTag: item.SeriesPrimaryImageTag - })); - - const episodesToInsert =await data.allEpisodes.filter(row => !existingEpisodes.includes((row.Id+row.ParentId))).map(item => ({ - Id: (item.Id+item.ParentId), - EpisodeId: item.Id, - Name: item.Name, - ServerId: item.ServerId, - PremiereDate: item.PremiereDate, - OfficialRating: item.OfficialRating, - CommunityRating: item.CommunityRating, - RunTimeTicks: item.RunTimeTicks, - ProductionYear: item.ProductionYear, - IndexNumber: item.IndexNumber, - ParentIndexNumber: item.ParentIndexNumber, - Type: item.Type, - ParentLogoItemId: item.ParentLogoItemId, - ParentBackdropItemId: item.ParentBackdropItemId, - ParentBackdropImageTags:item.ParentBackdropImageTags!==undefined ? item.ParentBackdropImageTags[0] : null, - SeriesId: item.SeriesId, - SeasonId: item.ParentId, - SeasonName: item.SeasonName, - SeriesName: item.SeriesName - })); - - - - - if(seasonsToInsert.length>0) - { - await (async () => { - // const client = await pool.connect(); - - try { - await db.query('BEGIN'); - - - //insert seasons - - const queryseasons = pgp.helpers.insert(seasonsToInsert, columnSeasons, 'jf_library_seasons'); - await db.query(queryseasons); - - // - - await db.query('COMMIT'); - console.log('Bulk insert successful'); - results.push({Status:'Success',Message:('Season insert successful for '+ show.Name)}); - } catch (error) { - await db.query('ROLLBACK'); - console.error('Error performing bulk insert:', error); - results.push({Status:'Error',Message:('Error performing bulk insert:', error)}); - } - })(); - }else{ - results.push({Status:'Information',Message:'No new seasons to insert for '+ show.Name}); - } - - - - if(episodesToInsert.length>0) - { - await (async () => { - // const client = await pool.connect(); - - try { - await db.query('BEGIN'); - - //insert episodes - - const queryepisodes = pgp.helpers.insert(episodesToInsert, columnEpisodes, 'jf_library_episodes'); - await db.query(queryepisodes); - - // - - await db.query('COMMIT'); - console.log('Bulk insert successful'); - results.push({Status:'Success',Message:('Episode insert successful for '+ show.Name)}); - } catch (error) { - await db.query('ROLLBACK'); - console.error('Error performing bulk insert:', error); - results.push({Status:'Error',Message:('Error performing bulk insert:', error)}); - } - })(); - }else{ - results.push({Status:'Information',Message:'No new episodes to insert for '+ show.Name}); - } - - - - } - - } else { - console.log("No shows found."); - results.push({Status:'Error',Message:'No shows found.'}); - - } - - - - - res.send(results); - - console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `); - -}); - - - -router.get('/writeLibraries', async (req, res) => { - - const sync = require('./sync'); - 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' }); - return; - } - - const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); - const data = await _sync.getLibraries(); - - // res.send(data); - // return; - - const columns = ['Id', 'Name', 'ServerId', 'IsFolder', 'Type', 'CollectionType', 'ImageTagsPrimary']; // specify the columns to insert into - - const existingIds = await db.query('SELECT "Id" FROM jf_libraries').then(res => res.rows.map(row => row.Id)); - - - const dataToInsert = data.filter(row => !existingIds.includes(row.Id)).map(item => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - IsFolder: item.IsFolder, - Type: item.Type, - CollectionType: item.CollectionType, - ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null - })); - - if(dataToInsert.length===0) - { - res.send('No new libraries to insert'); - return; - } - - (async () => { - // const client = await pool.connect(); - - try { - await db.query('BEGIN'); - - const query = pgp.helpers.insert(dataToInsert, columns, 'jf_libraries'); - await db.query(query); - - await db.query('COMMIT'); - console.log('Bulk insert successful'); - res.send('Bulk insert successful'); - } catch (error) { - await db.query('ROLLBACK'); - console.error('Error performing bulk insert:', error); - res.send(('Error performing bulk insert:', error)); - } - })(); - - - // res.send(results); - - - - console.log(`ENDPOINT CALLED: /writeLibraries: `); - -}); - -router.get('/writeLibraryItems', async (req, res) => { - - const sync = require('./sync'); - 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' }); - return; - } - - const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); - const data = await _sync.getAllItems(); - const existingIds = await db.query('SELECT "Id" FROM jf_library_items').then(res => res.rows.map(row => row.Id)); - - // res.send(data); - // return; - - const columns = ['Id', 'Name', 'ServerId', 'PremiereDate', 'EndDate', 'CommunityRating', 'RunTimeTicks', 'ProductionYear', 'IsFolder', 'Type', 'Status', 'ImageTagsPrimary', 'ImageTagsBanner', 'ImageTagsLogo', 'ImageTagsThumb', 'BackdropImageTags', 'ParentId']; // specify the columns to insert into - - const dataToInsert = data.filter(row => !existingIds.includes(row.Id)).map(item => ({ - Id: item.Id, - Name: item.Name, - ServerId: item.ServerId, - PremiereDate: item.PremiereDate, - EndDate: item.EndDate, - CommunityRating: item.CommunityRating, - RunTimeTicks: item.RunTimeTicks, - ProductionYear: item.ProductionYear, - IsFolder: item.IsFolder, - Type: item.Type, - Status: item.Status, - ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, - ImageTagsBanner: item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null, - ImageTagsLogo: item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null, - ImageTagsThumb: item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null, - BackdropImageTags: item.BackdropImageTags[0], - ParentId: item.ParentId - })); - - if(dataToInsert.length===0) - { - res.send('No new library Items to insert'); - return; - } - - (async () => { - // const client = await pool.connect(); - - try { - await db.query('BEGIN'); - await db.query('TRUNCATE TABLE jf_library_items'); - const query = pgp.helpers.insert(dataToInsert, columns, 'jf_library_items'); - await db.query(query); - - await db.query('COMMIT'); - console.log('Bulk insert successful'); - res.send('Bulk insert successful'); - } catch (error) { - await db.query('ROLLBACK'); - console.error('Error performing bulk insert:', error); - res.send(('Error performing bulk insert:', error)); - } - })(); - - - // res.send(results); - - - - console.log(`ENDPOINT CALLED: /writeLibraryItems: `); - }); module.exports = router; diff --git a/backend/server.js b/backend/server.js index 0621555..c86ace0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,6 +2,8 @@ const express = require('express'); const cors = require('cors'); const apiRouter = require('./api'); +const syncRouter = require('./sync'); +const statsRouter = require('./stats'); const app = express(); const PORT = process.env.PORT || 3003; @@ -10,6 +12,8 @@ const LISTEN_IP = '127.0.0.1'; app.use(express.json()); // middleware to parse JSON request bodies app.use(cors()); app.use('/api', apiRouter); // mount the API router at /api +app.use('/sync', syncRouter); // mount the API router at /sync +app.use('/stats', statsRouter); // mount the API router at /stats app.listen(PORT, () => { console.log(`Server listening on http://${LISTEN_IP}:${PORT}`); diff --git a/backend/stats.js b/backend/stats.js new file mode 100644 index 0000000..ccfbcb6 --- /dev/null +++ b/backend/stats.js @@ -0,0 +1,18 @@ +// api.js +const express = require("express"); +const db = require("./db"); + +const router = express.Router(); + +router.get("/test", async (req, res) => { + console.log(`ENDPOINT CALLED: /test`); + res.send("Backend Responded Succesfully"); +}); + +router.get("/getLibraryOverview", async (req, res) => { + const { rows } = await db.query('SELECT * FROM jf_library_count_view'); + res.send(rows); + console.log(`ENDPOINT CALLED: /getLibraryOverview`); +}); + +module.exports = router; diff --git a/backend/sync.js b/backend/sync.js index 7eaf6aa..b74e8eb 100644 --- a/backend/sync.js +++ b/backend/sync.js @@ -1,25 +1,34 @@ -// import { Component } from 'react'; -const axios = require('axios'); -// import Config from '../lib/config'; +const express = require("express"); +const pgp = require("pg-promise")(); +const db = require("./db"); +const axios = require("axios"); -class sync { +const ws = require("./WebsocketHandler"); +const sendMessageToClients = ws(8080); + +const router = express.Router(); + + + +/////////////////////////////////////////Functions +class sync { constructor(hostUrl, apiKey) { this.hostUrl = hostUrl; this.apiKey = apiKey; } - async getAdminUser() { - try { const url = `${this.hostUrl}/Users`; - console.log('getAdminUser: ',url); + console.log("getAdminUser: ", url); const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': this.apiKey, + "X-MediaBrowser-Token": this.apiKey, }, }); - const adminUser = response.data.filter(user => user.Policy.IsAdministrator === true); + const adminUser = response.data.filter( + (user) => user.Policy.IsAdministrator === true + ); return adminUser || null; } catch (error) { console.log(error); @@ -27,106 +36,697 @@ class sync { } } - async getAllItems() { - if( this.hostUrl ===undefined || this.apiKey ===undefined) - { - return('Error: Method Not Initialized'); - } - const libraries=await this.getLibraries(); - const allitems=[]; - for (let i = 0; i < libraries.length; i++) { - const item = libraries[i]; - let libraryItems=await this.getItem(item.Id); - // allitems.push({library:item, data:libraryItems}); - const libraryItemsWithParent = libraryItems.map(items => ({ ...items, ...{ParentId: item.Id} })); - // libraryItems["ParentId"]=item.Id; - allitems.push(...libraryItemsWithParent); - - } - return allitems; - - - } - - - async getLibraries() { - - try { - - const admins=await this.getAdminUser() - const userid=admins[0].Id; - const url = `${this.hostUrl}/users/${userid}/Items`; - console.log('getLibraries: ',url); - const response = await axios.get(url, { - headers: { - 'X-MediaBrowser-Token': this.apiKey, - }, - }); - const mediafolders = response.data.Items.filter(type => ['tvshows','movies'].includes(type.CollectionType)); - return mediafolders || null; - } catch (error) { - console.log(error); - return []; - } - } - async getItem(itemID) { - try { - - const admins=await this.getAdminUser() - const userid=admins[0].Id; - const url = `${this.hostUrl}/users/${userid}/Items?ParentID=${itemID}`; + const admins = await this.getAdminUser(); + const userid = admins[0].Id; + let url = `${this.hostUrl}/users/${userid}/Items`; + if (itemID !== undefined) { + url += `?ParentID=${itemID}`; + } const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': this.apiKey, + "X-MediaBrowser-Token": this.apiKey, }, }); - return response.data.Items; + + const results = response.data.Items; + if (itemID === undefined) { + return results.filter((type) => + ["tvshows", "movies"].includes(type.CollectionType) + ); + } else { + return results; + } } catch (error) { console.log(error); return []; } } + async getSeasonsAndEpisodes(showId) { + const allSeasons = []; + const allEpisodes = []; - async getShows() { - - let libraries=await this.getLibraries(); - libraries=libraries.filter(type => type.CollectionType==='tvshows'); - const allShows=[]; - for (let i = 0; i < libraries.length; i++) { - const item = libraries[i]; - const showItems=await this.getItem(item.Id); - const showsWithParent = showItems.map(items => ({ ...items, ...{ParentId: item.Id} })); - allShows.push(...showsWithParent); - } - return allShows; + let seasonItems = await this.getItem(showId); + const seasonWithParent = seasonItems.map((items) => ({ + ...items, + ...{ ParentId: showId }, + })); + allSeasons.push(...seasonWithParent); + for (let e = 0; e < seasonItems.length; e++) { + const season = seasonItems[e]; + let episodeItems = await this.getItem(season.Id); + const episodeWithParent = episodeItems.map((items) => ({ + ...items, + ...{ ParentId: season.Id }, + })); + allEpisodes.push(...episodeWithParent); + } + + return { allSeasons: allSeasons, allEpisodes: allEpisodes }; + } +} +////////////////////////////////////////API Methods + +///////////////////////////////////////writeLibraries +router.get("/writeLibraries", async (req, res) => { + let message = []; + + 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!" }); + return; } + const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY); + const data = await _sync.getItem(); //getting all root folders aka libraries - async getSeasonsAndEpisodes(showId) { - - const allSeasons=[]; - const allEpisodes=[]; + const columns = [ + "Id", + "Name", + "ServerId", + "IsFolder", + "Type", + "CollectionType", + "ImageTagsPrimary", + ]; // specify the columns to insert into + + const existingIds = await db + .query('SELECT "Id" FROM jf_libraries') + .then((res) => res.rows.map((row) => row.Id)); // get existing library Ids from the db + + //data mapping + const mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + ServerId: item.ServerId, + IsFolder: item.IsFolder, + Type: item.Type, + CollectionType: item.CollectionType, + ImageTagsPrimary: + item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, + }); + let dataToInsert = []; + //filter fix if jf_libraries is empty + + if (existingIds.length === 0) { + // if there are no existing Ids in the table, map all items in the data array to the expected format + dataToInsert = await data.map(mapping); + } else { + // otherwise, filter only new data to insert + dataToInsert = await data + .filter((row) => !existingIds.includes(row.Id)) + .map(mapping); + } + + //Bulkinsert new data not on db + if (dataToInsert.length !== 0) { + //insert new + await (async () => { + try { + await db.query("BEGIN"); + + const query = pgp.helpers.insert(dataToInsert, columns, "jf_libraries"); + await db.query(query); + + await db.query("COMMIT"); + message.push({ + Type: "Success", + Message: dataToInsert.length + " Rows Inserted.", + }); + sendMessageToClients(dataToInsert.length + " Rows Inserted."); + } catch (error) { + await db.query("ROLLBACK"); + message.push({ + Type: "Error", + Message: "Error performing bulk insert:" + error, + }); + sendMessageToClients({Message:"Error performing bulk insert:" + error}); + } + })(); + } else { + message.push({ Type: "Success", Message: "No new data to bulk insert" }); + sendMessageToClients({Message:"No new data to bulk insert"}); + } + //Bulk delete from db thats no longer on api + if (existingIds.length > data.length) { + await (async () => { + try { + await db.query("BEGIN"); + + const AllIds = data.map((row) => row.Id); + + const deleteQuery = { + text: `DELETE FROM jf_libraries WHERE "Id" NOT IN (${pgp.as.csv( + AllIds + )})`, + }; + const queries = [deleteQuery]; + for (let query of queries) { + await db.query(query); + } + + await db.query("COMMIT"); + + message.push({ + Type: "Success", + Message: existingIds.length - data.length + " Rows Removed.", + }); + sendMessageToClients(existingIds.length - data.length + " Rows Removed."); + } catch (error) { + await db.query("ROLLBACK"); + + message.push({ + Type: "Error", + Message: "Error performing bulk removal:" + error, + }); + sendMessageToClients({Message:"Error performing bulk removal:" + error}); + } + })(); + } else { + message.push({ Type: "Success", Message: "No new data to bulk delete" }); + sendMessageToClients({Message:"No new data to bulk delete"}); + } + //Sent logs - let seasonItems=await this.getItem(showId); - const seasonWithParent = seasonItems.map(items => ({ ...items, ...{ParentId: showId} })); - allSeasons.push(...seasonWithParent); - for (let e = 0; e < seasonItems.length; e++) { - const season = seasonItems[e]; - let episodeItems=await this.getItem(season.Id); - const episodeWithParent = episodeItems.map(items => ({ ...items, ...{ParentId: season.Id} })); - allEpisodes.push(...episodeWithParent); + res.send(message); - } - + console.log(`ENDPOINT CALLED: /writeLibraries: `); +}); - return {allSeasons:allSeasons,allEpisodes:allEpisodes}; -} +//////////////////////////////////////////////////////writeLibraryItems +router.get("/writeLibraryItems", async (req, res) => { + let message = []; + 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" }); + return; + } -} + const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); + //Get all Library items + //gets all libraries + const libraries = await _sync.getItem(); + const data = []; + //for each item in library run get item using that id as the ParentId (This gets the children of the parent id) + for (let i = 0; i < libraries.length; i++) { + const item = libraries[i]; + let libraryItems = await _sync.getItem(item.Id); + const libraryItemsWithParent = libraryItems.map((items) => ({ + ...items, + ...{ ParentId: item.Id }, + })); + data.push(...libraryItemsWithParent); + } + ///////////////////// -module.exports= sync; + const existingIds = await db + .query('SELECT "Id" FROM jf_library_items') + .then((res) => res.rows.map((row) => row.Id)); + + //Mappings to store data in DB + const columns = [ + "Id", + "Name", + "ServerId", + "PremiereDate", + "EndDate", + "CommunityRating", + "RunTimeTicks", + "ProductionYear", + "IsFolder", + "Type", + "Status", + "ImageTagsPrimary", + "ImageTagsBanner", + "ImageTagsLogo", + "ImageTagsThumb", + "BackdropImageTags", + "ParentId", + ]; // specify the columns to insert into + + //data mapping + const mapping = (item) => ({ + Id: item.Id, + Name: item.Name, + ServerId: item.ServerId, + PremiereDate: item.PremiereDate, + EndDate: item.EndDate, + CommunityRating: item.CommunityRating, + RunTimeTicks: item.RunTimeTicks, + ProductionYear: item.ProductionYear, + IsFolder: item.IsFolder, + Type: item.Type, + Status: item.Status, + ImageTagsPrimary: + item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null, + ImageTagsBanner: + item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null, + ImageTagsLogo: + item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null, + ImageTagsThumb: + item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null, + BackdropImageTags: item.BackdropImageTags[0], + ParentId: item.ParentId, + }); + let dataToInsert = []; + //filter fix if jf_libraries is empty + + if (existingIds.length === 0) { + // if there are no existing Ids in the table, map all items in the data array to the expected format + dataToInsert = await data.map(mapping); + } else { + // otherwise, filter only new data to insert + dataToInsert = await data + .filter((row) => !existingIds.includes(row.Id)) + .map(mapping); + } + + //Bulkinsert new data not on db + if (dataToInsert.length !== 0) { + //insert new + await (async () => { + try { + await db.query("BEGIN"); + + const query = pgp.helpers.insert( + dataToInsert, + columns, + "jf_library_items" + ); + await db.query(query); + + await db.query("COMMIT"); + message.push({ + Type: "Success", + Message: dataToInsert.length + " Rows Inserted.", + }); + } catch (error) { + await db.query("ROLLBACK"); + message.push({ + Type: "Error", + Message: "Error performing bulk insert:" + error, + }); + } + })(); + } else { + message.push({ Type: "Success", Message: "No new data to bulk insert" }); + } + //Bulk delete from db thats no longer on api + if (existingIds.length > data.length) { + await (async () => { + try { + await db.query("BEGIN"); + + const AllIds = data.map((row) => row.Id); + + const deleteQuery = { + text: `DELETE FROM jf_library_items WHERE "Id" NOT IN (${pgp.as.csv( + AllIds + )})`, + }; + const queries = [deleteQuery]; + for (let query of queries) { + await db.query(query); + } + + await db.query("COMMIT"); + + message.push({ + Type: "Success", + Message: existingIds.length - data.length + " Rows Removed.", + }); + } catch (error) { + await db.query("ROLLBACK"); + + message.push({ + Type: "Error", + Message: "Error performing bulk removal:" + error, + }); + } + })(); + } else { + message.push({ Type: "Success", Message: "No new data to bulk delete" }); + } + //Sent logs + + res.send(message); + + console.log(`ENDPOINT CALLED: /writeLibraryItems: `); +}); + +//////////////////////////////////////////////////////writeSeasonsAndEpisodes +router.get("/writeSeasonsAndEpisodes", async (req, res) => { + sendMessageToClients({color:'yellow',Message:"Beginning Seasons and Episode sync"}); + const message = []; + 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" }); + return; + } + + const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY); + const { rows: shows } = await db.query( + `SELECT * FROM public.jf_library_items where "Type"='Series'` + ); + + //loop for each show + for (const show of shows) { + const data = await _sync.getSeasonsAndEpisodes(show.Id); + + // + //get existing seasons and episodes + console.log(show.Id); + const existingIdsSeasons = await db + .query( + `SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'` + ) + .then((res) => res.rows.map((row) => row.Id)); + + let existingIdsEpisodes = []; + if (existingIdsSeasons.length > 0) { + existingIdsEpisodes = await db + .query( + `SELECT * FROM public.jf_library_episodes WHERE "SeasonId" IN (${existingIdsSeasons + .filter((seasons) => seasons !== "") + .map((seasons) => pgp.as.value(seasons)) + .map((value) => "'" + value + "'") + .join(", ")})` + ) + .then((res) => res.rows.map((row) => row.Id)); + } + + //Mappings to store data in DB + const columnSeasons = [ + "Id", + "Name", + "ServerId", + "IndexNumber", + "Type", + "ParentLogoItemId", + "ParentBackdropItemId", + "ParentBackdropImageTags", + "SeriesName", + "SeriesId", + "SeriesPrimaryImageTag", + ]; // specify the columns to insert into + const columnEpisodes = [ + "Id", + "EpisodeId", + "Name", + "ServerId", + "PremiereDate", + "OfficialRating", + "CommunityRating", + "RunTimeTicks", + "ProductionYear", + "IndexNumber", + "ParentIndexNumber", + "Type", + "ParentLogoItemId", + "ParentBackdropItemId", + "ParentBackdropImageTags", + "SeriesId", + "SeasonId", + "SeasonName", + "SeriesName", + ]; // specify the columns to insert into + + //data mapping + const seasonsmapping = (item) => ({ + Id: item.Id, + Name: item.Name, + ServerId: item.ServerId, + IndexNumber: item.IndexNumber, + Type: item.Type, + ParentLogoItemId: item.ParentLogoItemId, + ParentBackdropItemId: item.ParentBackdropItemId, + ParentBackdropImageTags: + item.ParentBackdropImageTags !== undefined + ? item.ParentBackdropImageTags[0] + : null, + SeriesName: item.SeriesName, + SeriesId: item.ParentId, + SeriesPrimaryImageTag: item.SeriesPrimaryImageTag, + }); + + const episodemapping = (item) => ({ + Id: item.Id + item.ParentId, + EpisodeId: item.Id, + Name: item.Name, + ServerId: item.ServerId, + PremiereDate: item.PremiereDate, + OfficialRating: item.OfficialRating, + CommunityRating: item.CommunityRating, + RunTimeTicks: item.RunTimeTicks, + ProductionYear: item.ProductionYear, + IndexNumber: item.IndexNumber, + ParentIndexNumber: item.ParentIndexNumber, + Type: item.Type, + ParentLogoItemId: item.ParentLogoItemId, + ParentBackdropItemId: item.ParentBackdropItemId, + ParentBackdropImageTags: + item.ParentBackdropImageTags !== undefined + ? item.ParentBackdropImageTags[0] + : null, + SeriesId: item.SeriesId, + SeasonId: item.ParentId, + SeasonName: item.SeasonName, + SeriesName: item.SeriesName, + }); + + // + + let seasonsToInsert = []; + let episodesToInsert = []; + //filter fix if jf_libraries is empty + + if (existingIdsSeasons.length === 0) { + // if there are no existing Ids in the table, map all items in the data array to the expected format + seasonsToInsert = await data.allSeasons.map(seasonsmapping); + } else { + // otherwise, filter only new data to insert + seasonsToInsert = await data.allSeasons + .filter((row) => !existingIdsSeasons.includes(row.Id)) + .map(seasonsmapping); + } + + if (existingIdsEpisodes.length === 0) { + // if there are no existing Ids in the table, map all items in the data array to the expected format + episodesToInsert = await data.allEpisodes.map(episodemapping); + } else { + // otherwise, filter only new data to insert + episodesToInsert = await data.allEpisodes + .filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId)) + .map(episodemapping); + } + + ///insert delete seasons + //Bulkinsert new data not on db + if (seasonsToInsert.length !== 0) { + //insert new + await (async () => { + try { + await db.query("BEGIN"); + + const query = pgp.helpers.insert( + seasonsToInsert, + columnSeasons, + "jf_library_seasons" + ); + await db.query(query); + + await db.query("COMMIT"); + message.push({ + Type: "Success", + Message: seasonsToInsert.length + " Rows Inserted for " + show.Name, + ItemId: show.Id, + TableName: "jf_library_seasons", + }); + sendMessageToClients({color:'cornflowerblue',Message:seasonsToInsert.length + " Rows Inserted for " + show.Name}); + } catch (error) { + await db.query("ROLLBACK"); + message.push({ + Type: "Error", + Message: "Error performing bulk insert:" + error, + ItemId: show.Id, + TableName: "jf_library_seasons", + }); + sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error}); + } + })(); + } else { + message.push({ + Type: "Success", + Message: "No new data to bulk insert for " + show.Name, + ItemId: show.Id, + TableName: "jf_library_seasons", + }); + sendMessageToClients({Message:"No new data to bulk insert for " + show.Name}); + } + //Bulk delete from db thats no longer on api + if (existingIdsSeasons.length > data.allSeasons.length) { + await (async () => { + try { + await db.query("BEGIN"); + + const AllIds = data.allSeasons.map((row) => row.Id); + + const deleteQuery = { + text: `DELETE FROM jf_library_seasons WHERE "Id" NOT IN (${pgp.as.csv( + AllIds + )})`, + }; + const queries = [deleteQuery]; + for (let query of queries) { + await db.query(query); + } + + await db.query("COMMIT"); + + message.push({ + Type: "Success", + Message: + existingIdsSeasons.length - + data.allSeasons.length + + " Rows Removed for " + + show.Name, + ItemId: show.Id, + TableName: "jf_library_seasons", + }); + sendMessageToClients({color:'orange',Message:existingIdsSeasons.length -data.allSeasons.length +" Rows Removed for " +show.Name}); + } catch (error) { + await db.query("ROLLBACK"); + + message.push({ + Type: "Error", + Message: "Error performing bulk removal:" + error, + ItemId: show.Id, + TableName: "jf_library_seasons", + }); + sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error}); + } + })(); + } else { + message.push({ + Type: "Success", + Message: "No new data to bulk delete for " + show.Name, + ItemId: show.Id, + TableName: "jf_library_seasons", + }); + sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); + } + //insert delete episodes + //Bulkinsert new data not on db + if (episodesToInsert.length !== 0) { + //insert new + await (async () => { + try { + await db.query("BEGIN"); + + const query = pgp.helpers.insert( + episodesToInsert, + columnEpisodes, + "jf_library_episodes" + ); + await db.query(query); + + await db.query("COMMIT"); + message.push({ + Type: "Success", + Message: + episodesToInsert.length + " Rows Inserted for " + show.Name, + ItemId: show.Id, + TableName: "jf_library_episodes", + }); + sendMessageToClients({color:'cornflowerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name}); + } catch (error) { + await db.query("ROLLBACK"); + message.push({ + Type: "Error", + Message: "Error performing bulk insert:" + error, + ItemId: show.Id, + TableName: "jf_library_episodes", + }); + sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error}); + } + })(); + } else { + message.push({ + Type: "Success", + Message: "No new data to bulk insert for " + show.Name, + ItemId: show.Id, + TableName: "jf_library_episodes", + }); + sendMessageToClients({Message:"No new data to bulk insert for " + show.Name}); + } + //Bulk delete from db thats no longer on api + if (existingIdsEpisodes.length > data.allEpisodes.length) { + await (async () => { + try { + await db.query("BEGIN"); + + const AllIds = data.allEpisodes.map((row) => row.Id + row.ParentId); + + const deleteQuery = { + text: `DELETE FROM jf_library_episodes WHERE "Id" NOT IN (${pgp.as.csv( + AllIds + )})`, + }; + const queries = [deleteQuery]; + for (let query of queries) { + await db.query(query); + } + + await db.query("COMMIT"); + + message.push({ + Type: "Success", + Message: + existingIdsEpisodes.length - + data.allEpisodes.length + + " Rows Removed for " + + show.Name, + ItemId: show.Id, + TableName: "jf_library_episodes", + }); + sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name}); + } catch (error) { + await db.query("ROLLBACK"); + + message.push({ + Type: "Error", + Message: "Error performing bulk removal:" + error, + ItemId: show.Id, + TableName: "jf_library_episodes", + }); + sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error}); + } + })(); + } else { + message.push({ + Type: "Success", + Message: "No new data to bulk delete for " + show.Name, + ItemId: show.Id, + TableName: "jf_library_episodes", + }); + sendMessageToClients({Message:"No new data to bulk delete for " + show.Name}); + } + } + sendMessageToClients({color:'lightgreen',Message:"Sync Complete"}); + res.send(message); + + console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `); +}); + +////////////////////////////////////// + +module.exports = router; diff --git a/package-lock.json b/package-lock.json index 8c271cd..ca0843d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "remixicon-react": "^1.0.0", "sequelize": "^6.29.0", "sqlite3": "^5.1.4", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "ws": "^8.13.0" } }, "node_modules/@adobe/css-tools": { @@ -12885,6 +12886,26 @@ } } }, + "node_modules/jsdom/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -19263,26 +19284,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-manifest-plugin": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz", @@ -19857,15 +19858,15 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -29181,6 +29182,14 @@ "whatwg-url": "^8.5.0", "ws": "^7.4.6", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } } }, "jsesc": { @@ -33647,12 +33656,6 @@ "ajv-formats": "^2.1.1", "ajv-keywords": "^5.0.0" } - }, - "ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "requires": {} } } }, @@ -34124,9 +34127,9 @@ } }, "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "requires": {} }, "xml-name-validator": { diff --git a/package.json b/package.json index 0d30069..ad53b37 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "remixicon-react": "^1.0.0", "sequelize": "^6.29.0", "sqlite3": "^5.1.4", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "ws": "^8.13.0" }, "scripts": { "start": "react-scripts start", diff --git a/src/classes/jellyfin-api.js b/src/classes/jellyfin-api.js index 28e6528..42f2b12 100644 --- a/src/classes/jellyfin-api.js +++ b/src/classes/jellyfin-api.js @@ -1,6 +1,6 @@ -import { Component } from 'react'; -import axios from 'axios'; -import Config from '../lib/config'; +import { Component } from "react"; +import axios from "axios"; +import Config from "../lib/config"; class API extends Component { constructor(props) { @@ -10,14 +10,13 @@ class API extends Component { }; } - async getSessions() { try { const config = await Config(); const url = `${config.hostUrl}/Sessions`; const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': config.apiKey, + "X-MediaBrowser-Token": config.apiKey, }, }); return response.data; @@ -28,16 +27,15 @@ class API extends Component { } async getActivityData(limit) { - if(limit===undefined || limit<1) - { - return[]; + if (limit === undefined || limit < 1) { + return []; } try { const config = await Config(); const url = `${config.hostUrl}/System/ActivityLog/Entries?limit=${limit}`; const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': config.apiKey, + "X-MediaBrowser-Token": config.apiKey, }, }); return response.data; @@ -53,10 +51,12 @@ class API extends Component { const url = `${config.hostUrl}/Users`; const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': config.apiKey, + "X-MediaBrowser-Token": config.apiKey, }, }); - const adminUser = response.data.filter(user => user.Policy.IsAdministrator === true); + const adminUser = response.data.filter( + (user) => user.Policy.IsAdministrator === true + ); return adminUser || null; } catch (error) { console.log(error); @@ -65,35 +65,35 @@ class API extends Component { } async getLibraries() { - try { const config = await Config(); - const admins=await this.getAdminUser() - const userid=admins[0].Id; + const admins = await this.getAdminUser(); + const userid = admins[0].Id; const url = `${config.hostUrl}/users/${userid}/Items`; const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': config.apiKey, + "X-MediaBrowser-Token": config.apiKey, }, }); - const mediafolders = response.data.Items.filter(type => ['tvshows','movies'].includes(type.CollectionType)); + const mediafolders = response.data.Items.filter((type) => + ["tvshows", "movies"].includes(type.CollectionType) + ); return mediafolders || null; } catch (error) { console.log(error); return []; } } - + async getItem(itemID) { - try { const config = await Config(); - const admins=await this.getAdminUser() - const userid=admins[0].Id; + const admins = await this.getAdminUser(); + const userid = admins[0].Id; const url = `${config.hostUrl}/users/${userid}/Items?ParentID=${itemID}`; const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': config.apiKey, + "X-MediaBrowser-Token": config.apiKey, }, }); return response.data.Items; @@ -103,14 +103,13 @@ class API extends Component { } } - async getRecentlyPlayed(userid,limit) { - + async getRecentlyPlayed(userid, limit) { try { const config = await Config(); const url = `${config.hostUrl}/users/${userid}/Items/Resume?limit=${limit}`; const response = await axios.get(url, { headers: { - 'X-MediaBrowser-Token': config.apiKey, + "X-MediaBrowser-Token": config.apiKey, }, }); return response.data.Items; @@ -119,8 +118,6 @@ class API extends Component { return []; } } - - } export default API; diff --git a/src/pages/activity.js b/src/pages/activity.js index f1ac6f5..5fdf37d 100644 --- a/src/pages/activity.js +++ b/src/pages/activity.js @@ -1,50 +1,44 @@ -import React, { useState, useEffect } from 'react'; -import API from '../classes/jellyfin-api'; - -import '../App.css' - -import Loading from './components/loading'; +import React, { useState, useEffect } from "react"; +import API from "../classes/jellyfin-api"; +import "../App.css"; +import Loading from "./components/loading"; function Activity() { const [data, setData] = useState([]); useEffect(() => { - let _api= new API() + let _api = new API(); const fetchData = () => { _api.getActivityData(30).then((ActivityData) => { - if (data && data.length > 0) - { - const newDataOnly = ActivityData.Items.filter(item => { - return !data.some(existingItem => existingItem.Id === item.Id); + if (data && data.length > 0) { + const newDataOnly = ActivityData.Items.filter((item) => { + return !data.some((existingItem) => existingItem.Id === item.Id); }); - setData([...newDataOnly, ...data.slice(0, data.length - newDataOnly.length)]); - } else - { + setData([ + ...newDataOnly, + ...data.slice(0, data.length - newDataOnly.length), + ]); + } else { setData(ActivityData.Items); } }); }; - const intervalId = setInterval(fetchData, 1000); return () => clearInterval(intervalId); }, [data]); - - - - const options = { - day: 'numeric', - month: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - hour12: false + day: "numeric", + month: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + hour12: false, }; if (!data || data.length === 0) { @@ -52,16 +46,25 @@ function Activity() { } return ( -
+

Activity Log

    {data && - data.map(item => ( -
  • items.Id === item.Id) <= 30 ? 'new' : 'old'}> - -
    {item.Name}
    -
    {new Date(item.Date).toLocaleString('en-GB', options).replace(',', '')}
    - + data.map((item) => ( +
  • items.Id === item.Id) <= 30 + ? "new" + : "old" + } + > +
    {item.Name}
    +
    + {new Date(item.Date) + .toLocaleString("en-GB", options) + .replace(",", "")} +
  • ))}
@@ -70,4 +73,3 @@ function Activity() { } export default Activity; - diff --git a/src/pages/components/libraryOverview.js b/src/pages/components/libraryOverview.js index 32e149a..46a2167 100644 --- a/src/pages/components/libraryOverview.js +++ b/src/pages/components/libraryOverview.js @@ -1,60 +1,76 @@ -import "../css/libraryOverview.css" -import React, { useState, useEffect } from 'react'; -import axios from "axios"; +import "../css/libraryOverview.css"; import Config from "../../lib/config"; +import React, { useState, useEffect } from "react"; +import axios from "axios"; import Loading from "./loading"; export default function LibraryOverView() { - const [data, setData] = useState([]); + const [data, setData] = useState([]); + const [base_url, setURL] = useState(""); + useEffect(() => { + if (base_url === "") { + Config() + .then((config) => { + setURL(config.hostUrl); + }) + .catch((error) => { + console.log(error); + }); + } + const fetchData = () => { + const url = `http://localhost:3003/stats/getLibraryOverview`; + axios + .get(url) + .then((response) => setData(response.data)) + .catch((error) => console.log(error)); + }; - useEffect(() => { - Config().then(config => { - const url = `${config.hostUrl}/Items/counts`; - const fetchData = () => { - axios.get(url, { - headers: { - 'X-MediaBrowser-Token': config.apiKey, - }, - }) - .then(response => setData(response.data)) - .catch(error => console.log(error)); - }; - - fetchData(); - const intervalId = setInterval(fetchData, 60000); - return () => clearInterval(intervalId); - }); - }, []); - - - - if (data.length === 0) { - return ; + if (!data || data.length === 0) { + fetchData(); } + + }, [data,base_url]); - return ( -
-
-
- {data.MovieCount + data.EpisodeCount} Media Files -
+ if (data.length === 0) { + return ; + } + + return ( +
+ {data && + data.map((stats) => ( +
+
+
+

Items in Library

{stats.Library_Count}

+
+ {stats.CollectionType === "tvshows" ? (
-
-

{data.MovieCount}

Movies

-
-
-

{data.BoxSetCount}

Box Sets

-
-
-

{data.SeriesCount}

Shows

-
-
-

{data.EpisodeCount}

Episodes

-
+

Seasons

{stats.Season_Count}

- + ) : ( + <> + )} + {stats.CollectionType === "tvshows" ? ( +
+

Episodes

{stats.Episode_Count}

+
+ ) : ( + <> + )}
-
- ) -} \ No newline at end of file +
+ ))} +
+ ); +} diff --git a/src/pages/components/recent-card.js b/src/pages/components/recent-card.js index 29d352d..831ab34 100644 --- a/src/pages/components/recent-card.js +++ b/src/pages/components/recent-card.js @@ -1,69 +1,76 @@ -import React from 'react'; +import React from "react"; function getLastPlayedTimeString(datetime) { - const now = new Date(); - const lastPlayed = new Date(datetime); - - const timeDifference = Math.abs(now.getTime() - lastPlayed.getTime()); - const yearsDifference = Math.floor(timeDifference / (1000 * 3600 * 24 * 365)); - const weeksDifference = Math.floor( - (timeDifference % (1000 * 3600 * 24 * 365)) / (1000 * 3600 * 24 * 7) - ); - const daysDifference = Math.floor( - (timeDifference % (1000 * 3600 * 24 * 7)) / (1000 * 3600 * 24) - ); - const hoursDifference = Math.floor( - (timeDifference % (1000 * 3600 * 24)) / (1000 * 3600) - ); - const minutesDifference = Math.floor( - (timeDifference % (1000 * 3600)) / (1000 * 60) - ); - - const timeUnits = [ - { label: "year", pluralLabel: "years", value: yearsDifference }, - { label: "week", pluralLabel: "weeks", value: weeksDifference }, - { label: "day", pluralLabel: "days", value: daysDifference }, - { label: "hour", pluralLabel: "hours", value: hoursDifference }, - { label: "minute", pluralLabel: "minutes", value: minutesDifference }, - ]; - - const timeString = timeUnits - .filter((unit) => unit.value > 0) - .map((unit, index, array) => { - const label = unit.value === 1 ? unit.label : unit.pluralLabel; - if (index === array.length - 1 && array.length > 1) { - // Special case for last time unit - return `and ${unit.value} ${label}`; - } else { - return `${unit.value} ${label}`; - } - }) - .join(" "); - - return `Watched ${timeString} ago`; - } + const now = new Date(); + const lastPlayed = new Date(datetime); + const timeDifference = Math.abs(now.getTime() - lastPlayed.getTime()); + const yearsDifference = Math.floor(timeDifference / (1000 * 3600 * 24 * 365)); + const weeksDifference = Math.floor( + (timeDifference % (1000 * 3600 * 24 * 365)) / (1000 * 3600 * 24 * 7) + ); + const daysDifference = Math.floor( + (timeDifference % (1000 * 3600 * 24 * 7)) / (1000 * 3600 * 24) + ); + const hoursDifference = Math.floor( + (timeDifference % (1000 * 3600 * 24)) / (1000 * 3600) + ); + const minutesDifference = Math.floor( + (timeDifference % (1000 * 3600)) / (1000 * 60) + ); + const timeUnits = [ + { label: "year", pluralLabel: "years", value: yearsDifference }, + { label: "week", pluralLabel: "weeks", value: weeksDifference }, + { label: "day", pluralLabel: "days", value: daysDifference }, + { label: "hour", pluralLabel: "hours", value: hoursDifference }, + { label: "minute", pluralLabel: "minutes", value: minutesDifference }, + ]; + + const timeString = timeUnits + .filter((unit) => unit.value > 0) + .map((unit, index, array) => { + const label = unit.value === 1 ? unit.label : unit.pluralLabel; + if (index === array.length - 1 && array.length > 1) { + // Special case for last time unit + return `and ${unit.value} ${label}`; + } else { + return `${unit.value} ${label}`; + } + }) + .join(" "); + + return `Watched ${timeString} ago`; +} function RecentCard(props) { - - return ( -
+
+
- -
+
+
{props.data.recent.Name}
+
+ {" "} + {getLastPlayedTimeString(props.data.recent.UserData.LastPlayedDate)}
- -
-
{props.data.recent.Name}
-
{getLastPlayedTimeString(props.data.recent.UserData.LastPlayedDate)}
-
); } -export default RecentCard; \ No newline at end of file +export default RecentCard; diff --git a/src/pages/components/recentlyplayed.js b/src/pages/components/recentlyplayed.js index f851e65..c11ad40 100644 --- a/src/pages/components/recentlyplayed.js +++ b/src/pages/components/recentlyplayed.js @@ -1,68 +1,58 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; // import axios from 'axios'; -import Config from '../../lib/config'; -import API from '../../classes/jellyfin-api'; +import Config from "../../lib/config"; +import API from "../../classes/jellyfin-api"; - -import "../css/recent.css" +import "../css/recent.css"; // import "../../App.css" +import RecentCard from "./recent-card"; -import RecentCard from './recent-card'; - - -import Loading from './loading'; - - - - +import Loading from "./loading"; function RecentlyPlayed() { const [data, setData] = useState([]); - const [base_url, setURL] = useState(''); + const [base_url, setURL] = useState(""); // const [errorHandler, seterrorHandler] = useState({ error_count: 0, error_message: '' }) - useEffect(() => { const _api = new API(); const fetchData = () => { - _api.getRecentlyPlayed('5f63950a2339462196eb8cead70cae7e',10).then((recentData) => { - setData(recentData); - }); + _api + .getRecentlyPlayed("5f63950a2339462196eb8cead70cae7e", 10) + .then((recentData) => { + setData(recentData); + }); }; - - Config().then(config => { - setURL(config.hostUrl); - }).catch(error => { - console.log(error); - } - ); + Config() + .then((config) => { + setURL(config.hostUrl); + }) + .catch((error) => { + console.log(error); + }); const intervalId = setInterval(fetchData, 1000); return () => clearInterval(intervalId); }, []); - - if (!data || data.length === 0) { return ; } - return ( - -
+
{data && - data.sort((a, b) => b.UserData.LastPlayedDate.localeCompare(a.UserData.LastPlayedDate)).map(recent => ( - - - - ))} + data + .sort((a, b) => + b.UserData.LastPlayedDate.localeCompare(a.UserData.LastPlayedDate) + ) + .map((recent) => ( + + ))}
- ); } export default RecentlyPlayed; - diff --git a/src/pages/components/session-card.js b/src/pages/components/session-card.js index d5a1cf5..8e263b4 100644 --- a/src/pages/components/session-card.js +++ b/src/pages/components/session-card.js @@ -1,14 +1,8 @@ -import React from 'react'; +import React from "react"; - -import AccountCircleFillIcon from 'remixicon-react/AccountCircleFillIcon'; -// import InformationFillIcon from 'remixicon-react/InformationFillIcon'; -import PlayFillIcon from 'remixicon-react/PlayFillIcon'; -import PauseFillIcon from 'remixicon-react/PauseFillIcon'; - -// import { Tooltip } from 'antd'; - -// import { Tooltip } from "@mui/material"; +import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon"; +import PlayFillIcon from "remixicon-react/PlayFillIcon"; +import PauseFillIcon from "remixicon-react/PauseFillIcon"; function ticksToTimeString(ticks) { // Convert ticks to seconds @@ -20,7 +14,9 @@ function ticksToTimeString(ticks) { const remainingSeconds = seconds % 60; // Format the time string as hh:MM:ss - const timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; + const timeString = `${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`; return timeString; } @@ -29,82 +25,200 @@ function sessionCard(props) { // Access data passed in as a prop using `props.data` if (props.data.session.NowPlayingItem === undefined) { - return( -
+ return ( +
+
- -
- -
- -
- -
- -
{props.data.session.DeviceName}
-
{props.data.session.Client + ' ' + props.data.session.ApplicationVersion}
+
+
+ +
+ {" "} + {props.data.session.DeviceName} +
+
+ {props.data.session.Client + + " " + + props.data.session.ApplicationVersion} +
-
- {(props.data.session.UserPrimaryImageTag !== undefined) ? : } -
{props.data.session.UserName}
- {/*
{props.data.session.RemoteEndPoint}
*/} +
+ {props.data.session.UserPrimaryImageTag !== undefined ? ( + + ) : ( + + )} +
{props.data.session.UserName}
-
-
-
- -
+
+
+
-
+
-
); } - // console.log(props.data); return ( -
- - -
- +
+
+
-
- -
- -
{props.data.session.DeviceName}
-
{props.data.session.Client + ' ' + props.data.session.ApplicationVersion}
+
+
+ +
+ {" "} + {props.data.session.DeviceName} +
+
+ {props.data.session.Client + + " " + + props.data.session.ApplicationVersion} +
-
- {(props.data.session.UserPrimaryImageTag !== undefined) ? : } -
{props.data.session.UserName}
- {/*
{props.data.session.RemoteEndPoint}
*/} +
+ {props.data.session.UserPrimaryImageTag !== undefined ? ( + + ) : ( + + )} +
{props.data.session.UserName}
-
{(props.data.session.PlayState.IsPaused) ? : } - {/* */} +
+ {props.data.session.PlayState.IsPaused ? ( + + ) : ( + + )} +
+
+ {" "} + {props.data.session.NowPlayingItem.Name}
-
{props.data.session.NowPlayingItem.Name}
- -
{ticksToTimeString(props.data.session.PlayState.PositionTicks)} / {ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)}
+
+ {" "} + {ticksToTimeString(props.data.session.PlayState.PositionTicks)} /{" "} + {ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)} +
-
+
-
); } -export default sessionCard; \ No newline at end of file +export default sessionCard; diff --git a/src/pages/components/sessions.js b/src/pages/components/sessions.js index 600d890..1d23f2c 100644 --- a/src/pages/components/sessions.js +++ b/src/pages/components/sessions.js @@ -1,27 +1,20 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react"; // import axios from 'axios'; -import Config from '../../lib/config'; -import API from '../../classes/jellyfin-api'; +import Config from "../../lib/config"; +import API from "../../classes/jellyfin-api"; -import "../css/sessions.css" +import "../css/sessions.css"; // import "../../App.css" +import SessionCard from "./session-card"; -import SessionCard from './session-card'; - - -import Loading from './loading'; - - - - +import Loading from "./loading"; function Sessions() { const [data, setData] = useState([]); - const [base_url, setURL] = useState(''); + const [base_url, setURL] = useState(""); // const [errorHandler, seterrorHandler] = useState({ error_count: 0, error_message: '' }) - useEffect(() => { const _api = new API(); const fetchData = () => { @@ -30,41 +23,36 @@ function Sessions() { }); }; - if(base_url==='') - { - Config().then(config => { - setURL(config.hostUrl); - }).catch(error => { - console.log(error); - } - ); + if (base_url === "") { + Config() + .then((config) => { + setURL(config.hostUrl); + }) + .catch((error) => { + console.log(error); + }); } - const intervalId = setInterval(fetchData, 1000); return () => clearInterval(intervalId); }, []); - - if (!data || data.length === 0) { return ; } - return ( - -
+
{data && - data.sort((a, b) => a.Id.padStart(12, '0').localeCompare(b.Id.padStart(12, '0'))).map(session => ( - - - - ))} + data + .sort((a, b) => + a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0")) + ) + .map((session) => ( + + ))}
- ); } export default Sessions; - diff --git a/src/pages/components/settings/WebSocketComponent .js b/src/pages/components/settings/WebSocketComponent .js new file mode 100644 index 0000000..2cd84e8 --- /dev/null +++ b/src/pages/components/settings/WebSocketComponent .js @@ -0,0 +1,49 @@ +import React, { useEffect, useState, useRef } from 'react'; +import '../../css/websocket/websocket.css'; + +const WebSocketComponent = () => { + const [messages, setMessages] = useState([]); + const messagesEndRef = useRef(null); + + useEffect(() => { + // create a new WebSocket connection + const socket = new WebSocket('ws://localhost:8080'); + + // handle incoming messages + socket.addEventListener('message', (event) => { + let message = JSON.parse(event.data); + setMessages(prevMessages => [...prevMessages, message]); + }); + + // cleanup function to close the WebSocket connection when the component unmounts + return () => { + socket.close(); + } + }, []); + + // function to handle scrolling to the last message + const scrollToBottom = () => { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + + // scroll to the last message whenever messages change + useEffect(() => { + scrollToBottom(); + }, [messages]); + + return ( +
+ {/*

WebSocket Example

*/} +
+ {messages.map((message, index) => ( +
+
{message.Message}
+
+ ))} +
+
+
+ ); +}; + +export default WebSocketComponent; diff --git a/src/pages/components/settings/librarySync.js b/src/pages/components/settings/librarySync.js new file mode 100644 index 0000000..278823a --- /dev/null +++ b/src/pages/components/settings/librarySync.js @@ -0,0 +1,40 @@ +import React from "react"; +import axios from "axios"; +// import Config from "../../../lib/config"; +// import Loading from "../loading"; + +import "../../css/settings.css"; + +export default function LibrarySync() { + + async function writeSeasonsAndEpisodes() { + // Send a GET request to /system/configuration to test copnnection + let isValid = false; + let errorMessage = ""; + await axios + .get("http://localhost:3003/sync/writeSeasonsAndEpisodes") + .then((response) => { + if (response.status === 200) { + isValid = true; + } + }) + .catch((error) => { + console.log(error); + }); + + return { isValid: isValid, errorMessage: errorMessage }; + } + + const handleClick = () => { + writeSeasonsAndEpisodes(); + console.log('Button clicked!'); + } + + return ( +
+ +
+ ); + + +} \ No newline at end of file diff --git a/src/pages/components/settings/settingsConfig.js b/src/pages/components/settings/settingsConfig.js new file mode 100644 index 0000000..2b86db8 --- /dev/null +++ b/src/pages/components/settings/settingsConfig.js @@ -0,0 +1,153 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../../../lib/config"; +import Loading from "../loading"; + +import "../../css/settings.css"; + +export default function SettingsConfig() { + + // const [config, setConfig] = useState({}); + const [formValues, setFormValues] = useState({}); + const [isSubmitted, setisSubmitted] = useState(""); + const [loadSate, setloadSate] = useState("Loading"); + const [submissionMessage, setsubmissionMessage] = useState(""); + + useEffect(() => { + Config() + .then((config) => { + setFormValues({ JF_HOST: config.hostUrl, JF_API_KEY: config.apiKey }); + setloadSate("Loaded"); + }) + .catch((error) => { + console.log("Error updating config:", error); + setloadSate("Critical"); + setsubmissionMessage( + "Error Retrieving Configuration. Unable to contact Backend Server" + ); + }); + }, []); + + async function validateSettings(_url, _apikey) { + let isValid = false; + let errorMessage = ""; + await axios + .get(_url + "/system/configuration", { + headers: { + "X-MediaBrowser-Token": _apikey, + }, + }) + .then((response) => { + if (response.status === 200) { + isValid = true; + } + }) + .catch((error) => { + // console.log(error.code); + if (error.code === "ERR_NETWORK") { + isValid = false; + errorMessage = `Error : Unable to connect to Jellyfin Server`; + } else if (error.response.status === 401) { + isValid = false; + errorMessage = `Error: ${error.response.status} Not Authorized. Please check API key`; + } else if (error.response.status === 404) { + isValid = false; + errorMessage = `Error ${error.response.status}: The requested URL was not found.`; + } else { + isValid = false; + errorMessage = `Error : ${error.response.status}`; + } + }); + + return { isValid: isValid, errorMessage: errorMessage }; + } + + async function handleFormSubmit(event) { + event.preventDefault(); + let validation = await validateSettings( + formValues.JF_HOST, + formValues.JF_API_KEY + ); + console.log(validation); + if (!validation.isValid) { + setisSubmitted("Failed"); + setsubmissionMessage(validation.errorMessage); + return; + } + + setisSubmitted(""); + + // Send a POST request to /api/setconfig/ with the updated configuration + axios + .post("http://localhost:3003/api/setconfig/", formValues, { + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + console.log("Config updated successfully:", response.data); + setisSubmitted("Success"); + setsubmissionMessage("Success Updated Configuration"); + }) + .catch((error) => { + console.log("Error updating config:", error); + setisSubmitted("Failed"); + setsubmissionMessage("Error Updating Configuration: ", error); + }); + } + + function handleFormChange(event) { + setFormValues({ ...formValues, [event.target.name]: event.target.value }); + } + if (loadSate === "Loading") { + return ; + } + + if (loadSate === "Critical") { + return
{submissionMessage}
; + } + + + + + return ( +
+
+ + +
+
+ + +
+ + {isSubmitted !== "" ? ( +
+ {submissionMessage} +
+ ) : ( + <> + )} + + +
+ ); + + +} \ No newline at end of file diff --git a/src/pages/css/libraryOverview.css b/src/pages/css/libraryOverview.css index f7fa7f1..9404441 100644 --- a/src/pages/css/libraryOverview.css +++ b/src/pages/css/libraryOverview.css @@ -1,39 +1,62 @@ -.overview{ - color: black; +.overview { + color: white; margin-top: 20px; font-family: 'Railway', sans-serif; font-weight: bold; -} - -.card -{ - /* width: 400px; */ + display: flex; + flex-direction: row; + + } + + .card { + width: 100%; + height: 200px; margin-right: 20px; - height: 170px; - background: linear-gradient(60deg, #AA5CC3 , #00A4DC); border-radius: 4px; text-align: center; + background-size: cover; + background-position: center; + margin-bottom: 5px; + background-color: black; + } + + .item-card-count { -} - -.item-card-count -{ - font-size: 40px; padding-top: 20px; padding-bottom: 10px; -} + } - - -.item-count -{ - font-size: 32px; - display: inline-block; + .item-count { + backdrop-filter: blur(2px); + font-size: 20px; + display: flex; + justify-content: center; + align-items: center; + height: 100%; padding-left: 30px; padding-right: 30px; -} - -p -{ + } + + .item-count > div { + display: inline-block; + margin-right: 10px; + text-align: center; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + .item-count > div p { margin: 0; -} + } + + .item-count > div:not(:last-child) { + margin-right: 10px; + } + + p { + margin: 0; + } + \ No newline at end of file diff --git a/src/pages/css/websocket/websocket.css b/src/pages/css/websocket/websocket.css new file mode 100644 index 0000000..7e18cdf --- /dev/null +++ b/src/pages/css/websocket/websocket.css @@ -0,0 +1,19 @@ +.console-container { + background-color: black; + color: white; + height: 500px; + overflow-y: auto; + margin-top: 20px; + padding: 10px; + margin-right: 10px; + border-radius: 4px; +} + +.console-message { + margin-bottom: 10px; +} + +.console-text { + margin: 0; + font-family: monospace; +} diff --git a/src/pages/libraries.js b/src/pages/libraries.js index b04afbf..cd974ba 100644 --- a/src/pages/libraries.js +++ b/src/pages/libraries.js @@ -1,95 +1,94 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import Config from '../lib/config'; - - -import './css/libraries.css' - -import Loading from './components/loading'; +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../lib/config"; +import "./css/libraries.css"; +import Loading from "./components/loading"; function Libraries() { - const [data, setData] = useState([]); - const [config, setConfig] = useState(null); + const [data, setData] = useState([]); + const [config, setConfig] = useState(null); - - useEffect(() => { - - - const fetchConfig = async () => { - try { - const newConfig = await Config(); - setConfig(newConfig); - } catch (error) { - if (error.code === 'ERR_NETWORK') { - console.log(error); - } - } - }; - - const fetchData = () => { - if (config) { - const url = `${config.hostUrl}/Library/MediaFolders`; - const apiKey = config.apiKey; - - axios.get(url, { - headers: { - 'X-MediaBrowser-Token': apiKey, - }, - }) - .then(data => { - console.log('data'); - setData(data.data.Items); - console.log(data); - }) - .catch(error => { - console.log(error); - }); - } - }; - - if (!config) { - fetchConfig(); + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config(); + setConfig(newConfig); + } catch (error) { + if (error.code === "ERR_NETWORK") { + console.log(error); } + } + }; - if (data.length === 0) { - fetchData(); - } + const fetchData = () => { + if (config) { + const url = `${config.hostUrl}/Library/MediaFolders`; + const apiKey = config.apiKey; + axios + .get(url, { + headers: { + "X-MediaBrowser-Token": apiKey, + }, + }) + .then((data) => { + console.log("data"); + setData(data.data.Items); + console.log(data); + }) + .catch((error) => { + console.log(error); + }); + } + }; - const intervalId = setInterval(fetchData, (60000*60)); - return () => clearInterval(intervalId); - }, [data, config]); - - - - - - if (!data || data.length === 0) { - return ; + if (!config) { + fetchConfig(); } - return ( -
-

Libraries

-
    - {data && - data.filter(collection => ['tvshows', 'movies'].includes(collection.CollectionType)).map(item => ( -
  • + if (data.length === 0) { + fetchData(); + } - {/*
    {item.Name}
    */} -
    - -
    + const intervalId = setInterval(fetchData, 60000 * 60); + return () => clearInterval(intervalId); + }, [data, config]); + if (!data || data.length === 0) { + return ; + } -
  • - ))} -
-
- ); + return ( +
+

Libraries

+
    + {data && + data + .filter((collection) => + ["tvshows", "movies"].includes(collection.CollectionType) + ) + .map((item) => ( +
  • + {/*
    {item.Name}
    */} +
    + +
    +
  • + ))} +
+
+ ); } export default Libraries; - diff --git a/src/pages/settings.js b/src/pages/settings.js index 0692680..c9727db 100644 --- a/src/pages/settings.js +++ b/src/pages/settings.js @@ -1,143 +1,20 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import Config from '../lib/config'; -import Loading from './components/loading'; +import React from "react"; -import './css/settings.css' +import SettingsConfig from "./components/settings/settingsConfig"; +import LibrarySync from "./components/settings/librarySync"; +import WebSocketComponent from "./components/settings/WebSocketComponent "; +import "./css/settings.css"; export default function Settings() { - // const [config, setConfig] = useState({}); - const [formValues, setFormValues] = useState({}); - const [isSubmitted, setisSubmitted] = useState(''); - const [loadSate, setloadSate] = useState('Loading'); - const [submissionMessage, setsubmissionMessage] = useState(''); - - - useEffect(() => { - Config().then(config => { - setFormValues({ JF_HOST: config.hostUrl, JF_API_KEY: config.apiKey }); - setloadSate('Loaded'); - }).catch(error => { - console.log('Error updating config:', error); - setloadSate('Critical'); - setsubmissionMessage('Error Retrieving Configuration. Unable to contact Backend Server'); - }); - }, []); - - async function validateSettings(_url, _apikey) { - // Send a GET request to /system/configuration to test copnnection - let isValid = false; - let errorMessage = ''; - await axios.get(_url + '/system/configuration', { - headers: { - 'X-MediaBrowser-Token': _apikey, - }, - }) - .then(response => { - // console.log('HTTP status code:', response.status); // logs the HTTP status code - //console.log('Response data:', response.data); // logs the response data - - if (response.status === 200) { - isValid = true; - } - - - }) - .catch(error => { - // console.log(error.code); - if (error.code === 'ERR_NETWORK') { - isValid = false; - errorMessage = `Error : Unable to connect to Jellyfin Server`; - } else - if (error.response.status === 401) { - isValid = false; - errorMessage = `Error: ${error.response.status} Not Authorized. Please check API key`; - } else - if (error.response.status === 404) { - isValid = false; - errorMessage = `Error ${error.response.status}: The requested URL was not found.`; - } else { - isValid = false; - errorMessage = `Error : ${error.response.status}`; - } - - }); - - return ({ isValid: isValid, errorMessage: errorMessage }); - } - - async function handleFormSubmit(event) { - event.preventDefault(); - let validation = await validateSettings(formValues.JF_HOST, formValues.JF_API_KEY); - console.log(validation); - if (!validation.isValid) { - setisSubmitted('Failed'); - setsubmissionMessage(validation.errorMessage); - return; - - } - - setisSubmitted(''); - - // Send a POST request to /api/setconfig/ with the updated configuration - axios.post('http://localhost:3003/api/setconfig/', formValues, { - headers: { - 'Content-Type': 'application/json' - } - }) - .then(response => { - console.log('Config updated successfully:', response.data); - setisSubmitted('Success'); - setsubmissionMessage('Success Updated Configuration'); - }) - .catch(error => { - console.log('Error updating config:', error); - setisSubmitted('Failed'); - setsubmissionMessage('Error Updating Configuration: ', error); - }); - } - - function handleFormChange(event) { - - setFormValues({ ...formValues, [event.target.name]: event.target.value }); - } - if (loadSate === 'Loading') { - - return ( - - - ) - - } - - if (loadSate === 'Critical') { - - return ( -
{submissionMessage}
- - ) - - } return ( -
-
- - -
-
- - -
- - {isSubmitted !== '' ?
{submissionMessage}
: - <> - } - - -
- - ) -} \ No newline at end of file +
+ + + + +
+ ); +} diff --git a/src/pages/setup.js b/src/pages/setup.js index 3bd3df6..700896d 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import Config from '../lib/config'; +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../lib/config"; -import './css/setup.css' +import "./css/setup.css"; // import Loading from './components/loading'; @@ -10,114 +10,91 @@ function Setup() { const [config, setConfig] = useState(null); const [formValues, setFormValues] = useState({}); const [processing, setProcessing] = useState(false); - const [submitButtonText, setsubmitButtonText] = useState('Save'); + const [submitButtonText, setsubmitButtonText] = useState("Save"); function handleFormChange(event) { - setFormValues({ ...formValues, [event.target.name]: event.target.value }); } async function validateSettings(_url, _apikey) { // Send a GET request to /system/configuration to test copnnection let isValid = false; - let errorMessage = ''; - await axios.get(_url + '/system/configuration', { - headers: { - 'X-MediaBrowser-Token': _apikey, - }, - }) - .then(response => { - // console.log('HTTP status code:', response.status); // logs the HTTP status code - //console.log('Response data:', response.data); // logs the response data - + let errorMessage = ""; + await axios + .get(_url + "/system/configuration", { + headers: { + "X-MediaBrowser-Token": _apikey, + }, + }) + .then((response) => { if (response.status === 200) { isValid = true; } - - }) - .catch(error => { + .catch((error) => { // console.log(error.code); - if (error.code === 'ERR_NETWORK') { + if (error.code === "ERR_NETWORK") { isValid = false; errorMessage = `Unable to connect to Jellyfin Server`; - } else - if (error.response.status === 401) { - isValid = false; - errorMessage = `Error ${error.response.status} Not Authorized`; - } else - if (error.response.status === 404) { - isValid = false; - errorMessage = `Error ${error.response.status}: The requested URL was not found.`; - } else { - isValid = false; - errorMessage = `Error : ${error.response.status}`; - } - + } else if (error.response.status === 401) { + isValid = false; + errorMessage = `Error ${error.response.status} Not Authorized`; + } else if (error.response.status === 404) { + isValid = false; + errorMessage = `Error ${error.response.status}: The requested URL was not found.`; + } else { + isValid = false; + errorMessage = `Error : ${error.response.status}`; + } }); - return ({ isValid: isValid, errorMessage: errorMessage }); + return { isValid: isValid, errorMessage: errorMessage }; } async function handleFormSubmit(event) { setProcessing(true); - event.preventDefault(); - - - // if(formValues.JF_HOST=='' || formValues.JF_API_KEY=='') - // { - // setsubmitButtonText('Plea'); - // return; - // } - - - let validation = await validateSettings(formValues.JF_HOST, formValues.JF_API_KEY); + event.preventDefault(); + let validation = await validateSettings( + formValues.JF_HOST, + formValues.JF_API_KEY + ); if (!validation.isValid) { - setsubmitButtonText(validation.errorMessage); setProcessing(false); return; - } - // Send a POST request to /api/setconfig/ with the updated configuration - axios.post('http://localhost:3003/api/setconfig/', formValues, { - headers: { - 'Content-Type': 'application/json' - } - }) - .then(response => { - setsubmitButtonText('Settings Saved'); + axios + .post("http://localhost:3003/api/setconfig/", formValues, { + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + setsubmitButtonText("Settings Saved"); setProcessing(false); setTimeout(() => { - window.location.href = '/'; + window.location.href = "/"; }, 1000); - + return; }) - .catch(error => { - setsubmitButtonText('Error Saving Settings'); + .catch((error) => { + setsubmitButtonText("Error Saving Settings"); setProcessing(false); - }); - - - } - useEffect(() => { - - const fetchConfig = async () => { try { const newConfig = await Config(); setConfig(newConfig); } catch (error) { - if (error.code === 'ERR_NETWORK') { + if (error.code === "ERR_NETWORK") { console.log(error); } } @@ -126,32 +103,40 @@ function Setup() { if (!config) { fetchConfig(); } - }, [config]); - - return (
-
- +
+

Setup

+
+ + +
+
+ + +
- -

Setup

-
- - - -
-
- - -
- - -
- + +
); diff --git a/src/pages/useractivity.js b/src/pages/useractivity.js index 5f83929..74e541e 100644 --- a/src/pages/useractivity.js +++ b/src/pages/useractivity.js @@ -1,140 +1,138 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import Config from '../lib/config'; +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import Config from "../lib/config"; +import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon"; -import AccountCircleFillIcon from 'remixicon-react/AccountCircleFillIcon'; - -import './css/usersactivity.css' - -import Loading from './components/loading'; - +import "./css/usersactivity.css"; +import Loading from "./components/loading"; function UserActivity() { - const [data, setData] = useState([]); - const [config, setConfig] = useState(null); - const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); + const [data, setData] = useState([]); + const [config, setConfig] = useState(null); + const [sortConfig, setSortConfig] = useState({ key: null, direction: null }); - function handleSort(key) { - const direction = sortConfig.key === key && sortConfig.direction === 'ascending' ? 'descending' : 'ascending'; - setSortConfig({ key, direction }); - } + function handleSort(key) { + const direction = + sortConfig.key === key && sortConfig.direction === "ascending" + ? "descending" + : "ascending"; + setSortConfig({ key, direction }); + } - function sortData(data, { key, direction }) { - if (!key) return data; + function sortData(data, { key, direction }) { + if (!key) return data; - const sortedData = [...data]; + const sortedData = [...data]; - sortedData.sort((a, b) => { - if (a[key] < b[key]) return direction === 'ascending' ? -1 : 1; - if (a[key] > b[key]) return direction === 'ascending' ? 1 : -1; - return 0; - }); + sortedData.sort((a, b) => { + if (a[key] < b[key]) return direction === "ascending" ? -1 : 1; + if (a[key] > b[key]) return direction === "ascending" ? 1 : -1; + return 0; + }); - return sortedData; - } + return sortedData; + } - useEffect(() => { - - - const fetchConfig = async () => { - try { - const newConfig = await Config(); - setConfig(newConfig); - } catch (error) { - if (error.code === 'ERR_NETWORK') { - console.log(error); - } - } - }; - - const fetchData = () => { - if (config) { - const url = `${config.hostUrl}/user_usage_stats/user_activity?days=9999`; - const apiKey = config.apiKey; - - axios.get(url, { - headers: { - 'X-MediaBrowser-Token': apiKey, - }, - }) - .then(data => { - console.log('data'); - setData(data.data); - console.log(data); - }) - .catch(error => { - console.log(error); - }); - } - }; - - if (!config) { - fetchConfig(); + useEffect(() => { + const fetchConfig = async () => { + try { + const newConfig = await Config(); + setConfig(newConfig); + } catch (error) { + if (error.code === "ERR_NETWORK") { + console.log(error); } + } + }; - if (data.length === 0) { - fetchData(); - } + const fetchData = () => { + if (config) { + const url = `${config.hostUrl}/user_usage_stats/user_activity?days=9999`; + const apiKey = config.apiKey; + axios + .get(url, { + headers: { + "X-MediaBrowser-Token": apiKey, + }, + }) + .then((data) => { + console.log("data"); + setData(data.data); + console.log(data); + }) + .catch((error) => { + console.log(error); + }); + } + }; - const intervalId = setInterval(fetchData, 60000); - return () => clearInterval(intervalId); - }, [data, config]); - - - - - // const options = { - // day: 'numeric', - // month: 'numeric', - // year: 'numeric', - // hour: 'numeric', - // minute: 'numeric', - // second: 'numeric', - // hour12: true - // }; - - if (!data || data.length === 0) { - return ; + if (!config) { + fetchConfig(); } - const sortedData = sortData(data, sortConfig); - return ( -
-

Users

- - - - {/* */} - - - - - - - - - - - { - sortedData.map(item => ( - - - - - - - - - {/* */} - - ))} - -
handleSort('user_id')}>User ID handleSort('user_name')}>User handleSort('item_name')}>Last Watched handleSort('client_name')}>Last Client handleSort('total_count')}>Total Plays handleSort('total_play_time')}>Total Watch Time handleSort('last_seen')}>Last Seen
{(item.has_image) ? : }{item.user_name}{item.item_name}{item.client_name}{item.total_count}{item.total_play_time}{item.last_seen} ago{new Date(item.latest_date).toLocaleString('en-GB', options).replace(',', '')}
-
- ); + + if (data.length === 0) { + fetchData(); + } + + const intervalId = setInterval(fetchData, 60000); + return () => clearInterval(intervalId); + }, [data, config]); + + if (!data || data.length === 0) { + return ; + } + const sortedData = sortData(data, sortConfig); + return ( +
+

Users

+ + + + + + + + + + + + + + {sortedData.map((item) => ( + + + + + + + + + + ))} + +
handleSort("user_name")}>User handleSort("item_name")}>Last Watched handleSort("client_name")}>Last Client handleSort("total_count")}>Total Plays handleSort("total_play_time")}> + Total Watch Time + handleSort("last_seen")}>Last Seen
+ {item.has_image ? ( + + ) : ( + + )} + {item.user_name}{item.item_name}{item.client_name}{item.total_count}{item.total_play_time}{item.last_seen} ago
+
+ ); } export default UserActivity; -