diff --git a/backend/api.js b/backend/api.js index 522c6a6..b096d7f 100644 --- a/backend/api.js +++ b/backend/api.js @@ -3,6 +3,17 @@ const express = require("express"); const axios = require("axios"); const ActivityMonitor=require('./watchdog/ActivityMonitor'); const db = require("./db"); +const https = require('https'); + +const agent = new https.Agent({ + rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true' +}); + + + +const axios_instance = axios.create({ + httpsAgent: agent +}); const router = express.Router(); @@ -154,7 +165,14 @@ router.post("/getItemDetails", async (req, res) => { query ); - res.send(episodes); + if(episodes.length!==0) + { + res.send(episodes); + }else + { + res.status(404).send('Item not found'); + } + }else{ @@ -250,20 +268,16 @@ router.post("/getItemHistory", async (req, res) => { ("EpisodeId"='${itemid}' OR "SeasonId"='${itemid}' OR "NowPlayingItemId"='${itemid}');` ); - const groupedResults = {}; - rows.forEach(row => { - if (groupedResults[row.NowPlayingItemId+row.EpisodeId]) { - groupedResults[row.NowPlayingItemId+row.EpisodeId].results.push(row); - } else { - groupedResults[row.NowPlayingItemId+row.EpisodeId] = { - ...row, - results: [] - }; - groupedResults[row.NowPlayingItemId+row.EpisodeId].results.push(row); - } - }); + + + const groupedResults = rows.map(item => ({ + ...item, + results: [] + })); - res.send(Object.values(groupedResults)); + + + res.send(groupedResults); } catch (error) { console.log(error); res.send(error); @@ -307,7 +321,7 @@ router.get("/getAdminUsers", async (req, res) => { try { const { rows:config } = await db.query('SELECT * FROM app_config where "ID"=1'); const url = `${config[0].JF_HOST}/Users`; - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": config[0].JF_API_KEY, }, @@ -339,6 +353,87 @@ router.get("/runWatchdog", async (req, res) => { } }); +router.get("/getSessions", async (req, res) => { + try { + + + 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; + } + + + + let url=`${config[0].JF_HOST}/sessions`; + + const response_data = await axios_instance.get(url, { + headers: { + "X-MediaBrowser-Token": config[0].JF_API_KEY , + }, + }); + res.send(response_data.data); + } catch (error) { + console.log(error); + res.send(error); + } +}); + +router.post("/validateSettings", async (req, res) => { + const { url,apikey } = req.body; + + let isValid = false; + let errorMessage = ""; + try + { + await axios_instance + .get(url + "/system/configuration", { + headers: { + "X-MediaBrowser-Token": apikey, + }, + }) + .then((response) => { + if (response.status === 200) { + isValid = true; + } + }) + .catch((error) => { + if (error.code === "ERR_NETWORK") { + isValid = false; + errorMessage = `Error : Unable to connect to Jellyfin Server`; + } else if (error.code === "ECONNREFUSED") { + isValid = false; + errorMessage = `Error : Unable to connect to Jellyfin Server`; + } + else if (error.response && error.response.status === 401) { + isValid = false; + errorMessage = `Error: ${error.response.status} Not Authorized. Please check API key`; + } else if (error.response && error.response.status === 404) { + isValid = false; + errorMessage = `Error ${error.response.status}: The requested URL was not found.`; + } else { + isValid = false; + errorMessage = `${error}`; + } + }); + + }catch(error) + { + isValid = false; + errorMessage = `Error: ${error}`; + } + + + res.send({isValid:isValid,errorMessage:errorMessage }); + + +}); + + + + + diff --git a/backend/backup.js b/backend/backup.js index bffae20..19e2cec 100644 --- a/backend/backup.js +++ b/backend/backup.js @@ -187,24 +187,32 @@ router.get('/restore/:filename', async (req, res) => { router.get('/files', (req, res) => { - const directoryPath = path.join(__dirname, backupfolder); - fs.readdir(directoryPath, (err, files) => { - if (err) { - res.status(500).send('Unable to read directory'); - } else { - const fileData = files.filter(file => file.endsWith('.json')) - .map(file => { - const filePath = path.join(directoryPath, file); - const stats = fs.statSync(filePath); - return { - name: file, - size: stats.size, - datecreated: stats.birthtime - }; - }); - res.json(fileData); - } - }); + try + { + const directoryPath = path.join(__dirname, backupfolder); + fs.readdir(directoryPath, (err, files) => { + if (err) { + res.status(500).send('Unable to read directory'); + } else { + const fileData = files.filter(file => file.endsWith('.json')) + .map(file => { + const filePath = path.join(directoryPath, file); + const stats = fs.statSync(filePath); + return { + name: file, + size: stats.size, + datecreated: stats.birthtime + }; + }); + res.json(fileData); + } + }); + + }catch(error) + { + console.log(error); + } + }); @@ -216,9 +224,10 @@ router.get('/restore/:filename', async (req, res) => { //delete backup router.delete('/files/:filename', (req, res) => { - const filePath = path.join(__dirname, backupfolder, req.params.filename); - + try{ + const filePath = path.join(__dirname, backupfolder, req.params.filename); + fs.unlink(filePath, (err) => { if (err) { console.error(err); diff --git a/backend/migrations/029_jf_all_user_activity_view.js b/backend/migrations/029_jf_all_user_activity_view.js new file mode 100644 index 0000000..334b1d1 --- /dev/null +++ b/backend/migrations/029_jf_all_user_activity_view.js @@ -0,0 +1,66 @@ +exports.up = async function(knex) { + await knex.raw(` + DROP VIEW jf_all_user_activity; + CREATE OR REPLACE VIEW jf_all_user_activity AS + SELECT u."Id" AS "UserId", + u."PrimaryImageTag", + u."Name" AS "UserName", + CASE + WHEN j."SeriesName" IS NULL THEN j."NowPlayingItemName" + ELSE (j."SeriesName" || ' - '::text) || j."NowPlayingItemName" + END AS "LastWatched", + CASE + WHEN j."SeriesName" IS NULL THEN j."NowPlayingItemId" + ELSE j."EpisodeId" + END AS "NowPlayingItemId", + j."ActivityDateInserted" AS "LastActivityDate", + (j."Client" || ' - '::text) || j."DeviceName" AS "LastClient", + plays."TotalPlays", + plays."TotalWatchTime", + now() - j."ActivityDateInserted" AS "LastSeen" + FROM ( + SELECT jf_users."Id", + jf_users."Name", + jf_users."PrimaryImageTag", + jf_users."LastLoginDate", + jf_users."LastActivityDate", + jf_users."IsAdministrator" + FROM jf_users + ) u + LEFT JOIN LATERAL ( + SELECT jf_playback_activity."Id", + jf_playback_activity."IsPaused", + jf_playback_activity."UserId", + jf_playback_activity."UserName", + jf_playback_activity."Client", + jf_playback_activity."DeviceName", + jf_playback_activity."DeviceId", + jf_playback_activity."ApplicationVersion", + jf_playback_activity."NowPlayingItemId", + jf_playback_activity."NowPlayingItemName", + jf_playback_activity."SeasonId", + jf_playback_activity."SeriesName", + jf_playback_activity."EpisodeId", + jf_playback_activity."PlaybackDuration", + jf_playback_activity."ActivityDateInserted" + FROM jf_playback_activity + WHERE jf_playback_activity."UserId" = u."Id" + ORDER BY jf_playback_activity."ActivityDateInserted" DESC + LIMIT 1 + ) j ON true + LEFT JOIN LATERAL ( + SELECT count(*) AS "TotalPlays", + sum(jf_playback_activity."PlaybackDuration") AS "TotalWatchTime" + FROM jf_playback_activity + WHERE jf_playback_activity."UserId" = u."Id" + ) plays ON true + ORDER BY (now() - j."ActivityDateInserted"); + `).catch(function(error) { + console.error(error); + }); + }; + + exports.down = async function(knex) { + await knex.raw(`DROP VIEW jf_all_user_activity;`); + }; + \ No newline at end of file diff --git a/backend/proxy.js b/backend/proxy.js new file mode 100644 index 0000000..0c7e8ad --- /dev/null +++ b/backend/proxy.js @@ -0,0 +1,114 @@ +const express = require('express'); +const axios = require("axios"); +const db = require("./db"); +const https = require('https'); + +const agent = new https.Agent({ + rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true' +}); + + +const axios_instance = axios.create({ + httpsAgent: agent +}); + +const router = express.Router(); + +router.get('/Items/Images/Backdrop/', async(req, res) => { + const { id,fillWidth,quality } = 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) { + res.send({ error: "Config Details Not Found" }); + return; + } + + + let url=`${config[0].JF_HOST}/Items/${id}/Images/Backdrop?fillWidth=${fillWidth || 100}&quality=${quality || 100}`; + + + axios_instance.get(url, { + responseType: 'arraybuffer' + }) + .then((response) => { + res.set('Content-Type', 'image/jpeg'); + res.status(200); + + if (response.headers['content-type'].startsWith('image/')) { + res.send(response.data); + } else { + res.send(response.data.toString()); + } + }) + .catch((error) => { + // console.error(error); + res.status(500).send('Error fetching image'); + }); + }); + + router.get('/Items/Images/Primary/', async(req, res) => { + const { id,fillWidth,quality } = 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) { + res.send({ error: "Config Details Not Found" }); + return; + } + + + let url=`${config[0].JF_HOST}/Items/${id}/Images/Primary?fillWidth=${fillWidth || 100}&quality=${quality || 100}`; + + axios_instance.get(url, { + responseType: 'arraybuffer' + }) + .then((response) => { + res.set('Content-Type', 'image/jpeg'); + res.status(200); + + if (response.headers['content-type'].startsWith('image/')) { + res.send(response.data); + } else { + res.send(response.data.toString()); + } + }) + .catch((error) => { + // console.error(error); + res.status(500).send('Error fetching image'); + }); + }); + + + router.get('/Users/Images/Primary/', async(req, res) => { + const { id,fillWidth,quality } = 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) { + res.send({ error: "Config Details Not Found" }); + return; + } + + + let url=`${config[0].JF_HOST}/Users/${id}/Images/Primary?fillWidth=${fillWidth || 100}&quality=${quality || 100}`; + + axios_instance.get(url, { + responseType: 'arraybuffer' + }) + .then((response) => { + res.set('Content-Type', 'image/jpeg'); + res.status(200); + + if (response.headers['content-type'].startsWith('image/')) { + res.send(response.data); + } else { + res.send(response.data.toString()); + } + }) + .catch((error) => { + // console.error(error); + res.status(500).send('Error fetching image'); + }); + }); + + + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index 9a59194..81e9d3d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,11 +7,15 @@ const knexConfig = require('./migrations'); const authRouter= require('./auth'); const apiRouter = require('./api'); +const proxyRouter = require('./proxy'); const syncRouter = require('./sync'); const statsRouter = require('./stats'); const backupRouter = require('./backup'); const ActivityMonitor = require('./watchdog/ActivityMonitor'); +const { checkForUpdates } = require('./version-control'); + + const app = express(); const db = knex(knexConfig.development); @@ -49,6 +53,7 @@ function verifyToken(req, res, next) { app.use('/auth', authRouter); // mount the API router at /api, with JWT middleware app.use('/api', verifyToken, apiRouter); // mount the API router at /api, with JWT middleware +app.use('/proxy', proxyRouter); // mount the API router at /api, with JWT middleware app.use('/sync', verifyToken, syncRouter); // mount the API router at /sync, with JWT middleware app.use('/stats', verifyToken, statsRouter); // mount the API router at /stats, with JWT middleware app.use('/data', verifyToken, backupRouter); // mount the API router at /stats, with JWT middleware @@ -62,6 +67,7 @@ try{ db.migrate.latest().then(() => { app.listen(PORT, async () => { console.log(`Server listening on http://${LISTEN_IP}:${PORT}`); + checkForUpdates(); ActivityMonitor.ActivityMonitor(1000); }); }); diff --git a/backend/stats.js b/backend/stats.js index d9740b5..78e7880 100644 --- a/backend/stats.js +++ b/backend/stats.js @@ -1,6 +1,7 @@ // api.js const express = require("express"); const db = require("./db"); +const axios=require("axios"); const router = express.Router(); @@ -283,6 +284,44 @@ router.post("/getLibraryLastPlayed", async (req, res) => { } }); +router.get("/getRecentlyAdded", async (req, res) => { + try { + + const { libraryid } = req.query; + 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 adminurl = `${config[0].JF_HOST}/Users`; + + const response = await axios.get(adminurl, { + headers: { + "X-MediaBrowser-Token": config[0].JF_API_KEY , + }, + }); + + const adminUser = response.data.filter( + (user) => user.Policy.IsAdministrator === true + ); + + + let url=`${config[0].JF_HOST}/users/${adminUser[0].Id}/Items/latest?parentId=${libraryid}`; + + const response_data = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": config[0].JF_API_KEY , + }, + }); + res.send(response_data.data); + } catch (error) { + console.log(error); + res.send(error); + } +}); + diff --git a/backend/sync.js b/backend/sync.js index 8b43234..158bd7f 100644 --- a/backend/sync.js +++ b/backend/sync.js @@ -2,6 +2,17 @@ const express = require("express"); const pgp = require("pg-promise")(); const db = require("./db"); const axios = require("axios"); +const https = require('https'); + +const agent = new https.Agent({ + rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true' +}); + + + +const axios_instance = axios.create({ + httpsAgent: agent +}); const wss = require("./WebsocketHandler"); const socket=wss; @@ -29,7 +40,7 @@ class sync { try { const url = `${this.hostUrl}/Users`; console.log("getAdminUser: ", url); - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, }, @@ -45,7 +56,7 @@ class sync { try { const url = `${this.hostUrl}/Users`; console.log("getAdminUser: ", url); - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, }, @@ -67,7 +78,7 @@ class sync { if (itemID !== undefined) { url += `?ParentID=${itemID}`; } - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, }, @@ -94,7 +105,7 @@ class sync { if (itemID !== undefined) { url += `?ParentID=${itemID}`; } - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, }, @@ -114,7 +125,7 @@ class sync { let url = `${this.hostUrl}/Items/${itemID}/playbackinfo?userId=${userid}`; - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": this.apiKey, }, @@ -337,10 +348,12 @@ async function syncShowItems() let deleteEpisodeCount = 0; //loop for each show + let show_counter=0; for (const show of shows) { 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'}); const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id)); @@ -431,7 +444,7 @@ async function syncShowItems() } - socket.sendMessageToClients({ Message: "Sync complete for " + show.Name }); + } socket.sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",}); @@ -595,7 +608,7 @@ async function syncPlaybackPluginData() const url = `${base_url}/user_usage_stats/submit_custom_query`; - const response = await axios.post(url, { + const response = await axios_instance.post(url, { CustomQueryString: query, }, { headers: { diff --git a/backend/version-control.js b/backend/version-control.js new file mode 100644 index 0000000..6f03a7a --- /dev/null +++ b/backend/version-control.js @@ -0,0 +1,33 @@ +const GitHub = require('github-api'); +const packageJson = require('../package.json'); + +async function checkForUpdates() { + const currentVersion = packageJson.version; + const repoOwner = 'cyfershepard'; + const repoName = 'jellystat'; + const gh = new GitHub(); + const repo = gh.getRepo(repoOwner, repoName); + let latestVersion; + + try { + const releases = await repo.listReleases(); + + if (releases.data.length > 0) { + latestVersion = releases.data[0].tag_name; + console.log(releases.data); + } + } catch (error) { + console.error(`Failed to fetch releases for ${repoName}: ${error.message}`); + } + + 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`); + } +} + +module.exports = { checkForUpdates }; diff --git a/backend/watchdog/ActivityMonitor.js b/backend/watchdog/ActivityMonitor.js index 17a4480..b01e911 100644 --- a/backend/watchdog/ActivityMonitor.js +++ b/backend/watchdog/ActivityMonitor.js @@ -1,10 +1,22 @@ const db = require("../db"); const pgp = require("pg-promise")(); const axios = require("axios"); + const moment = require('moment'); const { columnsPlayback, mappingPlayback } = require('../models/jf_playback_activity'); const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require('../models/jf_activity_watchdog'); const { randomUUID } = require('crypto'); +const https = require('https'); + +const agent = new https.Agent({ + rejectUnauthorized: (process.env.REJECT_SELF_SIGNED_CERTIFICATES || 'true').toLowerCase() ==='true' +}); + + + +const axios_instance = axios.create({ + httpsAgent: agent +}); async function ActivityMonitor(interval) { console.log("Activity Interval: " + interval); @@ -30,7 +42,7 @@ async function ActivityMonitor(interval) { } const url = `${base_url}/Sessions`; - const response = await axios.get(url, { + const response = await axios_instance.get(url, { headers: { "X-MediaBrowser-Token": apiKey, }, diff --git a/package-lock.json b/package-lock.json index d5012f6..2f04145 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "concurrently": "^7.6.0", "cors": "^2.8.5", "crypto-js": "^4.1.1", + "github-api": "^3.4.0", "http-proxy-middleware": "^2.0.6", "knex": "^2.4.2", "moment": "^2.29.4", @@ -38,6 +39,7 @@ "react-blurhash": "^0.3.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", "recharts": "^2.5.0", @@ -10034,6 +10036,38 @@ "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" }, + "node_modules/github-api": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/github-api/-/github-api-3.4.0.tgz", + "integrity": "sha512-2yYqYS6Uy4br1nw0D3VrlYWxtGTkUhIZrumBrcBwKdBOzMT8roAe8IvI6kjIOkxqxapKR5GkEsHtz3Du/voOpA==", + "dependencies": { + "axios": "^0.21.1", + "debug": "^2.2.0", + "js-base64": "^2.1.9", + "utf8": "^2.1.1" + } + }, + "node_modules/github-api/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/github-api/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/github-api/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/glob": { "version": "7.2.3", "license": "ISC", @@ -12918,6 +12952,11 @@ "topo": "3.x.x" } }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" + }, "node_modules/js-sdsl": { "version": "4.3.0", "license": "MIT", @@ -16406,6 +16445,25 @@ "version": "6.0.11", "license": "MIT" }, + "node_modules/react-fast-compare": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz", + "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "license": "MIT" @@ -16533,6 +16591,14 @@ } } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-smooth": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.2.tgz", @@ -18461,6 +18527,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf8": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz", + "integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT" diff --git a/package.json b/package.json index a180c2e..ebd0ff7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "concurrently": "^7.6.0", "cors": "^2.8.5", "crypto-js": "^4.1.1", + "github-api": "^3.4.0", "http-proxy-middleware": "^2.0.6", "knex": "^2.4.2", "moment": "^2.29.4", @@ -33,6 +34,7 @@ "react-blurhash": "^0.3.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", "react-router-dom": "^6.8.1", "react-scripts": "5.0.1", "recharts": "^2.5.0", diff --git a/src/App.css b/src/App.css index 82d1648..3a51e0d 100644 --- a/src/App.css +++ b/src/App.css @@ -1,5 +1,4 @@ - - +@import 'pages/css/variables.module.css'; main{ margin-inline: 20px; } @@ -17,7 +16,7 @@ main{ body { - background-color: #1e1c22 !important; + background-color: var(--background-color) !important; /* background-color: #17151a; */ color: white; } @@ -81,3 +80,28 @@ h2{ } +.btn-outline-primary +{ + color: grey!important; + border-color: var(--primary-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 63ac446..43d1ea2 100644 --- a/src/App.js +++ b/src/App.js @@ -1,10 +1,8 @@ // import logo from './logo.svg'; import './App.css'; import React, { useState, useEffect } from 'react'; -import { - Routes, - Route, -} from "react-router-dom"; +import { Routes, Route } from "react-router-dom"; +import { Helmet } from 'react-helmet'; import axios from 'axios'; import Config from './lib/config'; @@ -67,7 +65,7 @@ function App() { axios .get("/auth/isConfigured") .then(async (response) => { - console.log(response); + // console.log(response); if(response.status===200) { setisConfigured(true); @@ -119,6 +117,9 @@ if (config && config.apiKey ===null) { if (config && isConfigured && token!==null){ return (
+ + +
diff --git a/src/models/libraryItem.js b/src/models/libraryItem.js deleted file mode 100644 index 63c9962..0000000 --- a/src/models/libraryItem.js +++ /dev/null @@ -1,7 +0,0 @@ -class libraryItem { - constructor(id, name, email) { - this.id = id; - this.name = name; - this.email = email; - } - } \ No newline at end of file diff --git a/src/pages/components/activity/activity-table.js b/src/pages/components/activity/activity-table.js index 590b6fa..5e5c374 100644 --- a/src/pages/components/activity/activity-table.js +++ b/src/pages/components/activity/activity-table.js @@ -17,9 +17,11 @@ 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); // 1 hour = 3600 seconds const minutes = Math.floor((seconds % 3600) / 60); // 1 minute = 60 seconds let formattedTime=''; @@ -53,7 +55,7 @@ function Row(data) { hour: "numeric", minute: "numeric", second: "numeric", - hour12: false, + hour12: hour_format, }; @@ -75,7 +77,7 @@ function Row(data) { {row.Client} {Intl.DateTimeFormat('en-UK', options).format(new Date(row.ActivityDateInserted))} {formatTotalWatchTime(row.PlaybackDuration) || '0 minutes'} - {row.results.length} + {row.results.length !==0 ? row.results.length : 1} @@ -154,7 +156,7 @@ export default function ActivityTable(props) { {props.data .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) .map((row) => ( - + ))} {props.data.length===0 ? No Activity Found :''} diff --git a/src/pages/components/general/last-watched-card.js b/src/pages/components/general/last-watched-card.js index 4d2613a..9aeffb2 100644 --- a/src/pages/components/general/last-watched-card.js +++ b/src/pages/components/general/last-watched-card.js @@ -39,10 +39,9 @@ function LastWatchedCard(props) { setLoaded(true)} diff --git a/src/pages/components/item-info.js b/src/pages/components/item-info.js index 1843e0e..c5fa95b 100644 --- a/src/pages/components/item-info.js +++ b/src/pages/components/item-info.js @@ -12,6 +12,7 @@ import "../css/items/item-details.css"; import MoreItems from "./item-info/more-items"; import ItemActivity from "./item-info/item-activity"; +import ErrorPage from "./general/error"; import Config from "../../lib/config"; @@ -79,10 +80,12 @@ useEffect(() => { setData(itemData.data[0]); - setRefresh(false); + } catch (error) { + setData({notfound:true, message:error.response.data}); console.log(error); } + setRefresh(false); } }; @@ -101,18 +104,19 @@ useEffect(() => { -if(!data) -{ - return <>; -} -if(refresh) + +if(!data || refresh) { return ; } - - + +if(data && data.notfound) +{ + return ; +} + return (
@@ -123,10 +127,9 @@ if(refresh)
- {props.data.ImageBlurHashes && !loaded ? : null} + {(props.data.ImageBlurHashes || props.data.PrimaryImageHash )&& !loaded ? : null} {fallback ? setLoaded(true)} - style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }} - /> + src={ + `${ + "/Proxy/Items/Images/Primary?id=" + + Id + + "&fillHeight=320&fillWidth=213&quality=50"}` + } + alt="" + onLoad={() => setLoaded(true)} + style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }} + /> : setLoaded(true)} diff --git a/src/pages/components/library/RecentlyAdded/recently-added-card.js b/src/pages/components/library/RecentlyAdded/recently-added-card.js index 052ced6..2a6fd1d 100644 --- a/src/pages/components/library/RecentlyAdded/recently-added-card.js +++ b/src/pages/components/library/RecentlyAdded/recently-added-card.js @@ -15,11 +15,9 @@ function RecentlyAddedCard(props) { {loaded ? null : } setLoaded(true)} diff --git a/src/pages/components/library/library-card.js b/src/pages/components/library/library-card.js index f60d2fa..c4d23e9 100644 --- a/src/pages/components/library/library-card.js +++ b/src/pages/components/library/library-card.js @@ -80,24 +80,11 @@ function LibraryCard(props) {
- -
- diff --git a/src/pages/components/library/library-items.js b/src/pages/components/library/library-items.js index 36b82fb..a104112 100644 --- a/src/pages/components/library/library-items.js +++ b/src/pages/components/library/library-items.js @@ -1,5 +1,7 @@ import React, { useState, useEffect } from "react"; import axios from "axios"; +import {FormControl } from 'react-bootstrap'; + import MoreItemCards from "../item-info/more-items/more-items-card"; @@ -11,6 +13,7 @@ import "../../css/library/media-items.css"; function LibraryItems(props) { const [data, setData] = useState(); const [config, setConfig] = useState(); + const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { @@ -35,7 +38,6 @@ function LibraryItems(props) { }, }); setData(itemData.data); - console.log(itemData.data); } catch (error) { console.log(error); } @@ -53,6 +55,15 @@ function LibraryItems(props) { return () => clearInterval(intervalId); }, [config, props.LibraryId]); + let filteredData = data; + if(searchQuery) + { + filteredData = data.filter((item) => + item.Name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + } + + if (!data || !config) { return <>; @@ -60,9 +71,13 @@ function LibraryItems(props) { return (
+

Media

+ setSearchQuery(e.target.value)} className="my-3 w-25" /> +
+
- {data.map((item) => ( + {filteredData.map((item) => ( ))} diff --git a/src/pages/components/library/recently-added.js b/src/pages/components/library/recently-added.js index 1033357..2376b04 100644 --- a/src/pages/components/library/recently-added.js +++ b/src/pages/components/library/recently-added.js @@ -22,29 +22,30 @@ 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 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=`${config.hostUrl}/users/${adminId}/Items/latest?parentId=${props.LibraryId}`; + // let adminId=await fetchAdmin(); + let url=`/stats/getRecentlyAdded?libraryid=${props.LibraryId}`; const itemData = await axios.get(url, { headers: { - "X-MediaBrowser-Token": config.apiKey, + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", }, }); setData(itemData.data); diff --git a/src/pages/components/sessions/session-card.js b/src/pages/components/sessions/session-card.js index 61854b9..3340d59 100644 --- a/src/pages/components/sessions/session-card.js +++ b/src/pages/components/sessions/session-card.js @@ -33,7 +33,7 @@ function sessionCard(props) { // Access data passed in as a prop using `props.data` const cardStyle = { - backgroundImage: `url(${props.data.base_url}/Items/${(props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id)}/Images/Backdrop?fillHeight=320&fillWidth=213&quality=80), linear-gradient(to right, #00A4DC, #AA5CC3)`, + backgroundImage: `url(Proxy/Items/Images/Backdrop?id=${(props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id)}&fillHeight=320&fillWidth=213&quality=80), linear-gradient(to right, #00A4DC, #AA5CC3)`, height:'100%', backgroundSize: 'cover', }; @@ -53,7 +53,7 @@ function sessionCard(props) { @@ -105,11 +105,8 @@ function sessionCard(props) { { - const _api = new API(); - const fetchData = () => { - _api.getSessions().then((SessionData) => { - let results=SessionData.filter((session) => session.NowPlayingItem); - setData(results); - }); + const fetchConfig = async () => { + try { + const newConfig = await Config(); + setConfig(newConfig); + } catch (error) { + console.log(error); + } }; - if (base_url === "") { - Config() - .then((config) => { - setURL(config.hostUrl); + const fetchData = () => { + + if (config) { + const url = `/api/getSessions`; + + axios + .get(url, { + headers: { + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", + }, }) - .catch((error) => { - console.log(error); - }); + .then((data) => { + setData(data.data); + }) + .catch((error) => { + console.log(error); + }); + } + }; + + + if (!config) { + fetchConfig(); } if(!data) { @@ -41,7 +59,7 @@ function Sessions() { const intervalId = setInterval(fetchData, 1000); return () => clearInterval(intervalId); - }, [data,base_url]); + }, [data,config]); if (!data) { return ; @@ -63,11 +81,12 @@ function Sessions() {
{data && data + .filter(row => row.NowPlayingItem !== undefined) .sort((a, b) => a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0")) ) .map((session) => ( - + ))}
diff --git a/src/pages/components/settings/TerminalComponent.js b/src/pages/components/settings/TerminalComponent.js index e65ccb3..c217a2a 100644 --- a/src/pages/components/settings/TerminalComponent.js +++ b/src/pages/components/settings/TerminalComponent.js @@ -1,35 +1,39 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import '../../css/websocket/websocket.css'; const TerminalComponent = () => { const [messages, setMessages] = useState([]); - const messagesEndRef = useRef(null); useEffect(() => { - // create a new WebSocket connection - const socket = new WebSocket(`ws://${window.location.hostname+':'+(process.env.WS_PORT || 3004)}/ws`); + 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); + setMessages(message); }); - // cleanup function to close the WebSocket connection when the component unmounts return () => { socket.close(); } + + }catch(error) + { + // console.log(error); + } + }, []); return (
- {messages.map((message, index) => ( + {messages && messages.map((message, index) => (
{message.Message}
))} -
); diff --git a/src/pages/components/settings/backupfiles.js b/src/pages/components/settings/backupfiles.js index 9e0db87..3738808 100644 --- a/src/pages/components/settings/backupfiles.js +++ b/src/pages/components/settings/backupfiles.js @@ -128,14 +128,15 @@ function Row(file) { {data.name} {Intl.DateTimeFormat('en-UK', options).format(new Date(data.datecreated))} {formatFileSize(data.size)} - - - - downloadBackup(data.name)}>Download - restoreBackup(data.name)}>Restore - - deleteBackup(data.name)}>Delete - + +
+ + downloadBackup(data.name)}>Download + restoreBackup(data.name)}>Restore + + deleteBackup(data.name)}>Delete + +
@@ -229,7 +230,7 @@ const handlePreviousPageClick = () => { )} - + diff --git a/src/pages/components/settings/librarySync.js b/src/pages/components/settings/librarySync.js index 96542e4..3c84f25 100644 --- a/src/pages/components/settings/librarySync.js +++ b/src/pages/components/settings/librarySync.js @@ -66,8 +66,8 @@ export default function LibrarySync() { } return ( -
-

Tasks

+
+

Tasks

diff --git a/src/pages/components/settings/settingsConfig.js b/src/pages/components/settings/settingsConfig.js index 8371169..7055dc1 100644 --- a/src/pages/components/settings/settingsConfig.js +++ b/src/pages/components/settings/settingsConfig.js @@ -20,6 +20,7 @@ export default function SettingsConfig() { const [isSubmitted, setisSubmitted] = useState(""); const [loadSate, setloadSate] = useState("Loading"); const [submissionMessage, setsubmissionMessage] = useState(""); + const token = localStorage.getItem('token'); useEffect(() => { Config() @@ -38,37 +39,23 @@ export default function SettingsConfig() { }, []); 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}`; - } - }); + const result = await axios + .post("/api/validateSettings", { + url:_url, + apikey: _apikey - return { isValid: isValid, errorMessage: errorMessage }; + }, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + .catch((error) => { + let errorMessage= `Error : ${error}`; + }); + + let data=result.data; + return { isValid:data.isValid, errorMessage:data.errorMessage} ; } async function handleFormSubmit(event) { @@ -77,7 +64,7 @@ export default function SettingsConfig() { formValues.JF_HOST, formValues.JF_API_KEY ); - console.log(validation); + if (!validation.isValid) { setisSubmitted("Failed"); setsubmissionMessage(validation.errorMessage); @@ -119,7 +106,7 @@ export default function SettingsConfig() { return ( -
+

General Settings

diff --git a/src/pages/components/statCards/ItemStatComponent.js b/src/pages/components/statCards/ItemStatComponent.js index d6d0e3a..008f12c 100644 --- a/src/pages/components/statCards/ItemStatComponent.js +++ b/src/pages/components/statCards/ItemStatComponent.js @@ -12,10 +12,10 @@ function ItemStatComponent(props) { setLoaded(true); } - + const backgroundImage=`/proxy/Items/Images/Backdrop?id=${props.data[0].Id}&fillWidth=300&quality=10`; const cardStyle = { - backgroundImage: `url(${props.base_url}/Items/${props.data[0].Id}/Images/Backdrop/?fillWidth=300&quality=10), linear-gradient(to right, #00A4DC, #AA5CC3)`, + backgroundImage: `url(${backgroundImage}), linear-gradient(to right, #00A4DC, #AA5CC3)`, height:'100%', backgroundSize: 'cover', }; @@ -50,7 +50,7 @@ function ItemStatComponent(props) { )} setLoaded(false)} diff --git a/src/pages/components/user-info.js b/src/pages/components/user-info.js index f8b4207..9913643 100644 --- a/src/pages/components/user-info.js +++ b/src/pages/components/user-info.js @@ -80,10 +80,9 @@ function UserInfo() { button { color: white !important; - background-color: rgba(100,100, 100, 0.2); + background-color: var(--secondary-background-color); } @@ -38,7 +39,7 @@ td:hover > a } select option { - background-color: #4a4a4a; + background-color: var(--secondary-color); outline: unset; width: 100%; border: none; @@ -63,7 +64,7 @@ width: 130px; height: 35px; color: white; display: flex; -background-color: rgb(100, 100, 100,0.3); +background-color: var(--secondary-background-color); border-radius: 8px; font-size: 1.2em; align-self: flex-end; @@ -86,6 +87,6 @@ font-size: 1em; .page-btn { - background-color: rgb(90 45 165) !important; - border-color: rgb(90 45 165) !important; + background-color: var(--primary-color) !important; + border-color: var(--primary-color)!important; } \ No newline at end of file diff --git a/src/pages/css/error.css b/src/pages/css/error.css index 40274ee..f348791 100644 --- a/src/pages/css/error.css +++ b/src/pages/css/error.css @@ -1,13 +1,15 @@ .error { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; + margin: 0px; + height: calc(100vh - 100px); + display: flex; justify-content: center; align-items: center; + /* z-index: 9999; */ + background-color: #1e1c22; + transition: opacity 800ms ease-in; + opacity: 1; } .error .message diff --git a/src/pages/css/globalstats.css b/src/pages/css/globalstats.css index fe25a32..e01ab74 100644 --- a/src/pages/css/globalstats.css +++ b/src/pages/css/globalstats.css @@ -1,9 +1,11 @@ + +@import './variables.module.css'; .global-stats-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 400px)); grid-auto-rows: 120px; - background-color: rgb(100, 100, 100,0.2); + background-color: var(--secondary-background-color); padding: 20px; border-radius: 8px; font-size: 1.3em; @@ -31,7 +33,7 @@ .stat-value { text-align: right; - color: #00A4DC; + color: var(--secondary-color); font-weight: 500; font-size: 1.1em; margin: 0; diff --git a/src/pages/css/items/item-details.css b/src/pages/css/items/item-details.css index df83b20..9186acb 100644 --- a/src/pages/css/items/item-details.css +++ b/src/pages/css/items/item-details.css @@ -1,7 +1,8 @@ +@import '../variables.module.css'; .item-detail-container { color:white; - background-color: rgb(100, 100, 100,0.2); + background-color: var(--secondary-background-color); padding: 20px; margin: 20px 0; border-radius: 8px; @@ -26,6 +27,6 @@ } .item-details div a:hover{ - color: #00A4DC !important; + color: var(--secondary-color) !important; } \ No newline at end of file diff --git a/src/pages/css/lastplayed.css b/src/pages/css/lastplayed.css index 81e3d42..4c3168a 100644 --- a/src/pages/css/lastplayed.css +++ b/src/pages/css/lastplayed.css @@ -1,8 +1,9 @@ +@import './variables.module.css'; .last-played-container { display: flex; overflow-x: auto; - background-color: rgb(100, 100, 100,0.2); + background-color: var(--secondary-background-color); padding: 20px; border-radius: 8px; color: white; @@ -40,7 +41,7 @@ width: 150px; border-radius: 8px; - background-color: #1e1c22; + background-color: var(--background-color); } @@ -92,7 +93,7 @@ } .last-item-details a:hover{ - color: #00A4DC !important; + color: var(--secondary-color) !important; } @@ -120,6 +121,6 @@ .last-last-played{ font-size: 0.8em; margin-bottom: 5px; - color: #00a4dc; + color: var(--secondary-color); } diff --git a/src/pages/css/library/libraries.css b/src/pages/css/library/libraries.css index 80d1f60..ec0c8db 100644 --- a/src/pages/css/library/libraries.css +++ b/src/pages/css/library/libraries.css @@ -6,9 +6,3 @@ } -/* .library-banner-image -{ - border-radius: 5px; - max-width: 500px; - max-height: 500px; -} */ \ No newline at end of file diff --git a/src/pages/css/library/library-card.css b/src/pages/css/library/library-card.css index 3651974..07b557c 100644 --- a/src/pages/css/library/library-card.css +++ b/src/pages/css/library/library-card.css @@ -1,3 +1,5 @@ +@import '../variables.module.css'; + .lib-card{ color: white; max-width: 400px; @@ -6,7 +8,7 @@ .card-label { - color: #00A4DC; + color: var(--secondary-color); } .card-row .col @@ -39,7 +41,7 @@ .library-card-details { - background-color: rgb(100, 100, 100,0.2) !important; + background-color: var(--secondary-background-color) !important; } diff --git a/src/pages/css/library/media-items.css b/src/pages/css/library/media-items.css index 08d60cd..28ae0f3 100644 --- a/src/pages/css/library/media-items.css +++ b/src/pages/css/library/media-items.css @@ -1,10 +1,11 @@ +@import '../variables.module.css'; .media-items-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 150px)); grid-gap: 20px; - background-color: rgb(100, 100, 100,0.2); + background-color: var(--secondary-background-color); padding: 20px; border-radius: 8px; color: white; @@ -30,4 +31,18 @@ .media-items-container::-webkit-scrollbar-thumb:hover { background-color: #88888883; /* set thumb color */ + } + + .form-control + { + color: white !important; + background-color: var(--secondary-background-color) !important; + border-color: var(--secondary-background-color) !important; + } + + + .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/navbar.css b/src/pages/css/navbar.css index fe8e0eb..80150cf 100644 --- a/src/pages/css/navbar.css +++ b/src/pages/css/navbar.css @@ -1,12 +1,7 @@ +@import './variables.module.css'; .navbar { - /* display: flex; - justify-content: flex-end; - align-items: center; */ - background-color: #5a2da5; - /* background: linear-gradient(to right, #AA5CC3,#00A4DC); */ - /* height: 50px; */ - /* position: sticky; - top: 0; */ + background-color: var(--primary-color); + } diff --git a/src/pages/css/recent.css b/src/pages/css/recent.css index 1bcd718..6bf9900 100644 --- a/src/pages/css/recent.css +++ b/src/pages/css/recent.css @@ -18,15 +18,12 @@ display: flex; flex-direction: column; - /* background-color: grey; */ box-shadow: 0 0 20px rgba(255, 255, 255, 0.05); height: 320px; width: 185px; border-radius: 8px; - - /* Add a third row that takes up remaining space */ } @@ -46,9 +43,7 @@ width: 100%; height: 30%; position: relative; - /* margin: 8px; */ - /* background-color: #f71b1b; */ } .recent-card-item-name { @@ -58,10 +53,6 @@ position: absolute; margin: 0; - -/* - - position: absolute; */ } .recent-card-last-played{ diff --git a/src/pages/css/sessions.css b/src/pages/css/sessions.css index a97315f..e2e27eb 100644 --- a/src/pages/css/sessions.css +++ b/src/pages/css/sessions.css @@ -1,11 +1,9 @@ - +@import './variables.module.css'; .sessions-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 520px)); grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ - /* margin-right: 20px; */ - } .session-card { @@ -13,13 +11,10 @@ color: white; background-color: grey; - /* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */ max-height: 180px; max-width: 500px; - /* margin-left: 20px; */ - /* margin-bottom: 10px; */ background-size: cover; border-radius: 8px 8px 0px 8px; @@ -32,8 +27,6 @@ .progress-bar { - /* grid-row: 2 / 3; - grid-column: 1/3; */ height: 5px; background-color: #101010 !important; border-radius: 0px 0px 8px 8px; @@ -43,7 +36,7 @@ .progress-custom { height: 100%; - background-color: #00A4DC; + background-color: var(--secondary-color); transition: width 0.2s ease-in-out; border-radius: 0px 0px 0px 8px; } @@ -73,7 +66,6 @@ .card-banner-image { border-radius: 8px 0px 0px 0px; max-height: inherit; - /* box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.8); */ } .card-user { @@ -98,7 +90,6 @@ .card-user-image-default { - /* width: 50px !important; */ font-size: large; } @@ -125,7 +116,6 @@ display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; - /* grid-column-gap: 10px; */ } .card-device-name { @@ -137,8 +127,6 @@ .card-device-image { max-width: 35px; width: 100%; - /* margin-right: 5px; */ - /* grid-row: 1 / span 2; */ } .card-client { @@ -161,9 +149,6 @@ .card-playback-position { bottom: 5px; - /* right: 5px; */ - /* text-align: right; */ - /* position: absolute; */ } .device-info { @@ -181,6 +166,6 @@ margin-bottom: 100%; } .card-text a:hover{ - color: #00A4DC !important; + color: var(--secondary-color) !important; } \ No newline at end of file diff --git a/src/pages/css/settings/backups.css b/src/pages/css/settings/backups.css index 353fb2c..4ddb2d7 100644 --- a/src/pages/css/settings/backups.css +++ b/src/pages/css/settings/backups.css @@ -1,3 +1,4 @@ +@import '../variables.module.css'; tr{ color: white; } @@ -9,7 +10,6 @@ th:hover{ th{ border-bottom: none !important; cursor: default !important; - /* background-color: rgba(0, 0, 0, 0.8) !important; */ } .backup-file-download @@ -23,8 +23,8 @@ td{ .upload-file { - background-color: rgb(100, 100, 100,0.2) !important; - border-color: rgb(100, 100, 100,0.2) !important; + background-color: var(--secondary-background-color) !important; + border-color: var(--secondary-background-color) !important; color: white !important; } diff --git a/src/pages/css/settings/settings.css b/src/pages/css/settings/settings.css index c93debb..f20be35 100644 --- a/src/pages/css/settings/settings.css +++ b/src/pages/css/settings/settings.css @@ -1,29 +1,32 @@ +@import '../variables.module.css'; .show-key { margin-bottom: 20px;; } +.settings{ + background-color: var(--secondary-background-color); + padding: 20px; + border-radius: 8px; +} + +.tasks { + + color: white; + margin-inline: 10px; + +} + .settings-form { color: white; - /* width: 100%; */ margin-top: 20px; + margin-inline: 10px; } - .settings-submit-button - { - background-color: #2196f3; - color: white; - padding: 10px 20px; - border-radius: 5px; - border: none; - cursor: pointer; - transition: all 0.3s ease-in-out; - margin-bottom: 10px; - } .form-row { @@ -46,20 +49,6 @@ max-width: 700px; } - /* .settings-form button { - background-color: #2196f3; - color: white; - padding: 10px 20px; - border-radius: 5px; - border: none; - cursor: pointer; - transition: all 0.3s ease-in-out; - margin-bottom: 10px; - } */ - - /* .settings-form button:hover { - background-color: #0d8bf2; - } */ .submit { @@ -67,22 +56,4 @@ margin-bottom: 5px; } - .error - { - color: #cc0000; - } - - .success - { - color: #4BB543; - } - - .critical - { - display: flex; - justify-content: center; - align-items: center; - color: #cc0000; - - height: 90vh; - } \ No newline at end of file + \ No newline at end of file diff --git a/src/pages/css/statCard.css b/src/pages/css/statCard.css index 69054cd..4ed6e63 100644 --- a/src/pages/css/statCard.css +++ b/src/pages/css/statCard.css @@ -1,14 +1,14 @@ +@import './variables.module.css'; .grid-stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 520px)); grid-auto-rows: 200px;/* max-width+offset so 215 + 20*/ - /* margin-right: 20px; */ margin-top: 8px; } .stat-card{ border: 0 !important; - background-color: rgba(100 100 100 / 0.1) !important; + background-color: var(--background-color)!important; color: white; max-width: 500px; max-height: 180px; @@ -43,9 +43,8 @@ color: grey; } .stat-item-count { - /* width: 10%; */ text-align: right; - color: #00A4DC; + color: var(--secondary-color); font-weight: 500; font-size: 1.1em; @@ -69,7 +68,7 @@ height: 35px; color: white; display: flex; - background-color: rgb(100, 100, 100,0.3); + background-color: var(--secondary-background-color); border-radius: 8px; font-size: 1.2em; align-self: flex-end; @@ -119,5 +118,5 @@ input[type=number] { } .stat-items div a:hover{ - color: #00A4DC !important; + color: var(--secondary-color) !important; } diff --git a/src/pages/css/stats.css b/src/pages/css/stats.css index 3940fa4..ca7761b 100644 --- a/src/pages/css/stats.css +++ b/src/pages/css/stats.css @@ -1,3 +1,4 @@ +@import './variables.module.css'; .watch-stats { margin-top: 10px; @@ -8,11 +9,9 @@ height: 700px; color:black !important; - background-color:rgba(100,100,100,0.2); + background-color: var(--secondary-background-color); padding:10px; - border-radius:8px; - /* text-align: center; */ } diff --git a/src/pages/css/users/user-details.css b/src/pages/css/users/user-details.css index fe09105..6fdddac 100644 --- a/src/pages/css/users/user-details.css +++ b/src/pages/css/users/user-details.css @@ -1,7 +1,8 @@ +@import '../variables.module.css'; .user-detail-container { color:white; - background-color: rgb(100, 100, 100,0.2); + background-color: var(--secondary-background-color); padding: 20px; margin: 20px 0; border-radius: 8px; @@ -23,7 +24,7 @@ height: 100px; border-radius: 50%; object-fit: cover; - box-shadow: 0 0 10px 5px rgba(100,100,100,0.2); + box-shadow: 0 0 10px 5px var(--secondary-background-color); } .user-image-container diff --git a/src/pages/css/variables.module.css b/src/pages/css/variables.module.css new file mode 100644 index 0000000..e9a8739 --- /dev/null +++ b/src/pages/css/variables.module.css @@ -0,0 +1,6 @@ +:root { + --primary-color: #5a2da5; + --secondary-color: #00A4DC; + --background-color: #1e1c22; + --secondary-background-color: rgba(100, 100, 100,0.2); + } \ No newline at end of file diff --git a/src/pages/css/websocket/websocket.css b/src/pages/css/websocket/websocket.css index abe9804..c4a9001 100644 --- a/src/pages/css/websocket/websocket.css +++ b/src/pages/css/websocket/websocket.css @@ -15,12 +15,11 @@ .console-text { margin: 0; font-family: monospace; + text-overflow: ellipsis; + overflow-x: hidden; } -.console-container { - overflow: auto; /* show scrollbar when needed */ -} .console-container::-webkit-scrollbar { width: 10px; /* set scrollbar width */ @@ -37,4 +36,4 @@ .console-container::-webkit-scrollbar-thumb:hover { background-color: #88888883; /* set thumb color */ -} \ No newline at end of file +} diff --git a/src/pages/login.js b/src/pages/login.js index 307f5a3..b8542f9 100644 --- a/src/pages/login.js +++ b/src/pages/login.js @@ -91,6 +91,7 @@ function Login() { type="text" id="username" name="username" + autocomplete="on" value={formValues.username || ""} onChange={handleFormChange} required @@ -102,6 +103,7 @@ function Login() { type="text" id="password" name="password" + autocomplete="on" value={formValues.password || ""} onChange={handleFormChange} required diff --git a/src/pages/settings.js b/src/pages/settings.js index e1bfcd3..d455e45 100644 --- a/src/pages/settings.js +++ b/src/pages/settings.js @@ -1,4 +1,5 @@ import React from "react"; +import {Tabs, Tab } from 'react-bootstrap'; import SettingsConfig from "./components/settings/settingsConfig"; import LibrarySync from "./components/settings/librarySync"; @@ -15,11 +16,20 @@ export default function Settings() { return ( -
- - - +
+ + + + + + + + + + + +
diff --git a/src/pages/setup.js b/src/pages/setup.js index 21cd0de..9b0e7d1 100644 --- a/src/pages/setup.js +++ b/src/pages/setup.js @@ -3,6 +3,7 @@ import axios from "axios"; import Config from "../lib/config"; import "./css/setup.css"; +const token = localStorage.getItem('token'); // import LibrarySync from "./components/settings/librarySync"; // import Loading from './components/loading'; @@ -39,38 +40,23 @@ function Setup() { } 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) => { - if (response.status === 200) { - isValid = true; - } - }) - .catch((error) => { - // console.log(error.code); - 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} Unauthorized`; - } 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}`; - } - }); + const result = await axios + .post("/api/validateSettings", { + url:_url, + apikey: _apikey - return { isValid: isValid, errorMessage: errorMessage }; + }, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }) + .catch((error) => { + let errorMessage= `Error : ${error}`; + }); + + let data=result.data; + return { isValid:data.isValid, errorMessage:data.errorMessage} ; } async function handleFormSubmit(event) { diff --git a/src/pages/users.js b/src/pages/users.js index c64f1ad..b196a56 100644 --- a/src/pages/users.js +++ b/src/pages/users.js @@ -66,10 +66,9 @@ function Row(row) { @@ -78,7 +77,7 @@ function Row(row) { )} {data.UserName} - {data.LastWatched || 'never'} + {data.LastWatched || 'never'} {data.LastClient || 'n/a'} {data.TotalPlays} {formatTotalWatchTime(data.TotalWatchTime) || 0} @@ -189,9 +188,9 @@ function Users() { {data && data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) .map((row) => ( - + ))} - {data.length===0 ?
:''} + {data.length===0 ? :''}
No Backups Found
No Users Found
diff --git a/src/setupProxy.js b/src/setupProxy.js index 5b77631..6bdefa0 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -8,6 +8,13 @@ module.exports = function(app) { changeOrigin: true, }) ); + app.use( + `/proxy`, + createProxyMiddleware({ + target: `http://127.0.0.1:${process.env.PORT || 3003}`, + changeOrigin: true, + }) + ); app.use( `/stats`, createProxyMiddleware({ @@ -40,8 +47,9 @@ module.exports = function(app) { `/ws`, createProxyMiddleware({ target: `ws://127.0.0.1:${process.env.WS_PORT || 3004}`, - changeOrigin: true, ws: true, + changeOrigin: true, + secure: false, }) );