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)
-
-
-
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) => (
))}
-
);
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 ? | No Backups Found |
:''}
+ {data.length===0 ? | 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,
})
);