mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Fix for Proxy with SSL support
Added SSL proxying on the server side so that HTTP content can be served with HTTPS content Changed css colors from hardcoded values to use variables Changes to websockets to be able to work with SSL/Proxies WIP support of 12/24Hr time format in activities tables (Not working yet) WIP added version checker to be able to check for available updates from GitHub (Not working yet)
This commit is contained in:
125
backend/api.js
125
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 });
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
66
backend/migrations/029_jf_all_user_activity_view.js
Normal file
66
backend/migrations/029_jf_all_user_activity_view.js
Normal file
@@ -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;`);
|
||||
};
|
||||
|
||||
114
backend/proxy.js
Normal file
114
backend/proxy.js
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
33
backend/version-control.js
Normal file
33
backend/version-control.js
Normal file
@@ -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 };
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
71
package-lock.json
generated
71
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
30
src/App.css
30
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;
|
||||
|
||||
}
|
||||
|
||||
11
src/App.js
11
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 (
|
||||
<div className="App">
|
||||
<Helmet>
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
|
||||
</Helmet>
|
||||
<Navbar />
|
||||
<div>
|
||||
<main>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
class libraryItem {
|
||||
constructor(id, name, email) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
<TableCell>{row.Client}</TableCell>
|
||||
<TableCell>{Intl.DateTimeFormat('en-UK', options).format(new Date(row.ActivityDateInserted))}</TableCell>
|
||||
<TableCell>{formatTotalWatchTime(row.PlaybackDuration) || '0 minutes'}</TableCell>
|
||||
<TableCell>{row.results.length}</TableCell>
|
||||
<TableCell>{row.results.length !==0 ? row.results.length : 1}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={7}>
|
||||
@@ -154,7 +156,7 @@ export default function ActivityTable(props) {
|
||||
{props.data
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row) => (
|
||||
<Row key={row.Id} row={row} />
|
||||
<Row key={row.Id+row.NowPlayingItemId+row.EpisodeId} row={row} />
|
||||
))}
|
||||
{props.data.length===0 ? <tr><td colSpan="7" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}} className='py-2'>No Activity Found</td></tr> :''}
|
||||
|
||||
|
||||
@@ -39,10 +39,9 @@ function LastWatchedCard(props) {
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
"/Proxy/Items/Images/Primary?id=" +
|
||||
props.data.Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
"&fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
|
||||
@@ -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 <Loading/>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(data && data.notfound)
|
||||
{
|
||||
return <ErrorPage message={data.message}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -123,10 +127,9 @@ if(refresh)
|
||||
<img
|
||||
className="item-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
"/Proxy/Items/Images/Primary?id=" +
|
||||
(data.Type==="Episode"? data.SeriesId : data.Id) +
|
||||
"/Images/Primary?fillWidth=200&quality=90"
|
||||
"&fillWidth=200&quality=90"
|
||||
}
|
||||
alt=""
|
||||
style={{
|
||||
|
||||
@@ -11,34 +11,32 @@ function MoreItemCards(props) {
|
||||
const { Id } = useParams();
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [fallback, setFallback] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={props.data.Type==="Episode" ? "last-card episode-card" : "last-card"}>
|
||||
<Link to={`/libraries/item/${ (props.data.Type==="Episode" ? props.data.EpisodeId : props.data.Id) }`}>
|
||||
<div className={props.data.Type==="Episode" ? "last-card-banner episode" : "last-card-banner"}>
|
||||
{props.data.ImageBlurHashes && !loaded ? <Blurhash hash={props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]} width={'100%'} height={'100%'}/> : null}
|
||||
{(props.data.ImageBlurHashes || props.data.PrimaryImageHash )&& !loaded ? <Blurhash hash={props.data.PrimaryImageHash || props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary] } width={'100%'} height={'100%'}/> : null}
|
||||
|
||||
{fallback ?
|
||||
<img
|
||||
className="episode"
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => 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' }}
|
||||
/>
|
||||
:
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
"/Proxy/Items/Images/Primary?id=" +
|
||||
(props.data.Type==="Episode" ? props.data.EpisodeId : props.data.Id) +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
"&fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
|
||||
@@ -15,11 +15,9 @@ function RecentlyAddedCard(props) {
|
||||
{loaded ? null : <Blurhash hash={props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]} width={'100%'} height={'100%'}/>}
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
`${"/Proxy/Items/Images/Primary?id=" +
|
||||
props.data.Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
"&fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
|
||||
@@ -80,24 +80,11 @@ function LibraryCard(props) {
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className="library-card-banner"
|
||||
src={props.base_url + "/Items/" + props.data.Id + "/Images/Primary/?fillWidth=800&quality=50"}
|
||||
src={"/proxy/Items/Images/Primary?id=" + props.data.Id + "&fillWidth=800&quality=50"}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link to={`/libraries/${props.data.Id}`}>
|
||||
<div
|
||||
className="library-card-banner"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
props.data.Id +
|
||||
"/Images/Primary/?fillWidth=400&quality=90"
|
||||
})`,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Card.Body className="library-card-details">
|
||||
<Row className="space-between-end card-row">
|
||||
|
||||
@@ -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 (
|
||||
<div className="last-played">
|
||||
<div className="d-md-flex justify-content-between">
|
||||
<h1 className="my-3">Media</h1>
|
||||
<FormControl type="text" placeholder="Search" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="my-3 w-25" />
|
||||
</div>
|
||||
|
||||
<div className="media-items-container">
|
||||
{data.map((item) => (
|
||||
{filteredData.map((item) => (
|
||||
<MoreItemCards data={item} base_url={config.hostUrl} key={item.Id+item.SeasonNumber+item.EpisodeNumber}/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className="stat-card-image rounded-0"
|
||||
src={props.data.base_url + "/Items/" + (props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id) + "/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}
|
||||
src={"/Proxy/Items/Images/Primary?id=" + (props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id) + "&fillHeight=320&fillWidth=213&quality=50"}
|
||||
/>
|
||||
|
||||
|
||||
@@ -105,11 +105,8 @@ function sessionCard(props) {
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/Users/" +
|
||||
"/Proxy/Users/Images/Primary?id=" +
|
||||
props.data.session.UserId +
|
||||
"/Images/Primary?tag=" +
|
||||
props.data.session.UserPrimaryImageTag +
|
||||
"&quality=50"
|
||||
}
|
||||
alt=""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
// import axios from 'axios';
|
||||
import axios from 'axios';
|
||||
import Config from "../../../lib/config";
|
||||
import API from "../../../classes/jellyfin-api";
|
||||
// import API from "../../../classes/jellyfin-api";
|
||||
|
||||
import "../../css/sessions.css";
|
||||
// import "../../App.css"
|
||||
@@ -12,27 +12,45 @@ import Loading from "../general/loading";
|
||||
|
||||
function Sessions() {
|
||||
const [data, setData] = useState();
|
||||
const [base_url, setURL] = useState("");
|
||||
|
||||
const [config, setConfig] = useState();
|
||||
// const [errorHandler, seterrorHandler] = useState({ error_count: 0, error_message: '' })
|
||||
|
||||
useEffect(() => {
|
||||
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 <Loading />;
|
||||
@@ -63,11 +81,12 @@ function Sessions() {
|
||||
<div className="sessions-container">
|
||||
{data &&
|
||||
data
|
||||
.filter(row => row.NowPlayingItem !== undefined)
|
||||
.sort((a, b) =>
|
||||
a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0"))
|
||||
)
|
||||
.map((session) => (
|
||||
<SessionCard key={session.Id} data={{ session: session, base_url: base_url }} />
|
||||
<SessionCard key={session.Id} data={{ session: session, base_url: config.base_url }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className='my-4'>
|
||||
<div className="console-container">
|
||||
{messages.map((message, index) => (
|
||||
{messages && messages.map((message, index) => (
|
||||
<div key={index} className="console-message">
|
||||
<pre style={{color: message.color || 'white'}} className="console-text">{message.Message}</pre>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -128,14 +128,15 @@ function Row(file) {
|
||||
<TableCell>{data.name}</TableCell>
|
||||
<TableCell>{Intl.DateTimeFormat('en-UK', options).format(new Date(data.datecreated))}</TableCell>
|
||||
<TableCell>{formatFileSize(data.size)}</TableCell>
|
||||
<TableCell className="d-flex justify-content-center">
|
||||
<DropdownButton title="Actions" variant="outline-primary">
|
||||
|
||||
<Dropdown.Item as="button" variant="primary" onClick={()=>downloadBackup(data.name)}>Download</Dropdown.Item>
|
||||
<Dropdown.Item as="button" variant="warning" onClick={()=>restoreBackup(data.name)}>Restore</Dropdown.Item>
|
||||
<Dropdown.Divider ></Dropdown.Divider>
|
||||
<Dropdown.Item as="button" variant="danger" onClick={()=>deleteBackup(data.name)}>Delete</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
<TableCell className="">
|
||||
<div className="d-flex justify-content-center">
|
||||
<DropdownButton title="Actions" variant="outline-primary">
|
||||
<Dropdown.Item as="button" variant="primary" onClick={()=>downloadBackup(data.name)}>Download</Dropdown.Item>
|
||||
<Dropdown.Item as="button" variant="warning" onClick={()=>restoreBackup(data.name)}>Restore</Dropdown.Item>
|
||||
<Dropdown.Divider ></Dropdown.Divider>
|
||||
<Dropdown.Item as="button" variant="danger" onClick={()=>deleteBackup(data.name)}>Delete</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
|
||||
</TableCell>
|
||||
|
||||
@@ -229,7 +230,7 @@ const handlePreviousPageClick = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TableContainer className='rounded-2 overflow-visible'>
|
||||
<TableContainer className='rounded-2'>
|
||||
<Table aria-label="collapsible table" >
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
||||
@@ -66,8 +66,8 @@ export default function LibrarySync() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-form">
|
||||
<h1 className="my-2">Tasks</h1>
|
||||
<div className="tasks">
|
||||
<h1 >Tasks</h1>
|
||||
<Row className="mb-3">
|
||||
|
||||
<Form.Label column sm="2">
|
||||
|
||||
@@ -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 (
|
||||
<div className="general-settings-page">
|
||||
<div>
|
||||
<h1>General Settings</h1>
|
||||
<Form onSubmit={handleFormSubmit} className="settings-form">
|
||||
<Form.Group as={Row} className="mb-3" >
|
||||
|
||||
@@ -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) {
|
||||
)}
|
||||
<Card.Img
|
||||
className="stat-card-image"
|
||||
src={props.base_url + "/Items/" + props.data[0].Id + "/Images/Primary?fillWidth=400&quality=90"}
|
||||
src={"Proxy/Items/Images/Primary?id=" + props.data[0].Id + "& fillWidth=400&quality=90"}
|
||||
style={{ display: loaded ? 'block' : 'none' }}
|
||||
onLoad={handleImageLoad}
|
||||
onError={() => setLoaded(false)}
|
||||
|
||||
@@ -80,10 +80,9 @@ function UserInfo() {
|
||||
<img
|
||||
className="user-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Users/" +
|
||||
data.Id +
|
||||
"/Images/Primary?fillHeight=100&fillWidth=100&quality=90"
|
||||
"/Proxy/Users/Images/Primary?id=" +
|
||||
UserId+
|
||||
"&quality=100"
|
||||
}
|
||||
onError={handleImageError}
|
||||
alt=""
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@import '../variables.module.css';
|
||||
|
||||
.activity-table
|
||||
{
|
||||
background-color: rgba(100,100, 100, 0.2);
|
||||
background-color: var(--secondary-background-color);
|
||||
color: white !important;
|
||||
|
||||
}
|
||||
@@ -9,7 +10,7 @@
|
||||
td,th, td>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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,3 @@
|
||||
|
||||
}
|
||||
|
||||
/* .library-banner-image
|
||||
{
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
} */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; */
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
6
src/pages/css/variables.module.css
Normal file
6
src/pages/css/variables.module.css
Normal file
@@ -0,0 +1,6 @@
|
||||
:root {
|
||||
--primary-color: #5a2da5;
|
||||
--secondary-color: #00A4DC;
|
||||
--background-color: #1e1c22;
|
||||
--secondary-background-color: rgba(100, 100, 100,0.2);
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<SettingsConfig/>
|
||||
<BackupFiles/>
|
||||
<LibrarySync/>
|
||||
<div className="settings my-2">
|
||||
<Tabs defaultActiveKey="tabGeneral" variant='pills'>
|
||||
<Tab eventKey="tabGeneral" className='bg-transparent my-2' title='General Settings' style={{minHeight:'500px'}}>
|
||||
<SettingsConfig/>
|
||||
</Tab>
|
||||
<Tab eventKey="tabTasks" className='bg-transparent my-2' title='Tasks' style={{minHeight:'500px'}}>
|
||||
<LibrarySync/>
|
||||
|
||||
</Tab>
|
||||
<Tab eventKey="tabBackup" className='bg-transparent my-2' title='Backup' style={{minHeight:'500px'}}>
|
||||
<BackupFiles/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<TerminalComponent/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -66,10 +66,9 @@ function Row(row) {
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
row.hostUrl +
|
||||
"/Users/" +
|
||||
"Proxy/Users/Images/Primary?id=" +
|
||||
data.UserId +
|
||||
"/Images/Primary?quality=10"
|
||||
"&quality=10"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
@@ -78,7 +77,7 @@ function Row(row) {
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell><Link to={`/users/${data.UserId}`} className="text-decoration-none">{data.UserName}</Link></TableCell>
|
||||
<TableCell>{data.LastWatched || 'never'}</TableCell>
|
||||
<TableCell><Link to={`/libraries/item/${data.NowPlayingItemId}`} className="text-decoration-none">{data.LastWatched || 'never'}</Link></TableCell>
|
||||
<TableCell>{data.LastClient || 'n/a'}</TableCell>
|
||||
<TableCell>{data.TotalPlays}</TableCell>
|
||||
<TableCell>{formatTotalWatchTime(data.TotalWatchTime) || 0}</TableCell>
|
||||
@@ -189,9 +188,9 @@ function Users() {
|
||||
<TableBody>
|
||||
{data && data.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row) => (
|
||||
<Row key={row.id} data={row} hostUrl={config.hostUrl}/>
|
||||
<Row key={row.UserId} data={row} hostUrl={config.hostUrl}/>
|
||||
))}
|
||||
{data.length===0 ? <tr><td colSpan="5" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}}>No Backups Found</td></tr> :''}
|
||||
{data.length===0 ? <tr><td colSpan="5" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}}>No Users Found</td></tr> :''}
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user