mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-03-18 21:30:35 +01:00
bck/frnt end changes to accomade sync and stats
This commit is contained in:
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -1,16 +1,19 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://10.0.0.20:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"name": "Run Script: start",
|
||||
"request": "launch",
|
||||
"command": "npm run start",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
backend/WebsocketHandler.js
Normal file
48
backend/WebsocketHandler.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const WebSocket = require('ws');
|
||||
|
||||
function createWebSocketServer(port) {
|
||||
const wss = new WebSocket.Server({ port });
|
||||
let connected = false;
|
||||
|
||||
// function to handle WebSocket connections
|
||||
|
||||
function handleConnection(ws) {
|
||||
if (!connected) {
|
||||
console.log('Client connected');
|
||||
connected = true;
|
||||
|
||||
// listen for messages from the client
|
||||
ws.on('message', (message) => {
|
||||
console.log(`Received message: ${message}`);
|
||||
});
|
||||
|
||||
// listen for close events
|
||||
ws.on('close', () => {
|
||||
console.log('Client disconnected');
|
||||
connected = false;
|
||||
});}
|
||||
else
|
||||
{
|
||||
console.log('WebSocket connection already established');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// call the handleConnection function for each new WebSocket connection
|
||||
wss.on('connection', handleConnection);
|
||||
|
||||
// define a separate method that sends a message to all connected clients
|
||||
function sendMessageToClients(message) {
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
let parsedMessage = JSON.stringify(message);
|
||||
console.log(parsedMessage);
|
||||
client.send(parsedMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sendMessageToClients;
|
||||
}
|
||||
|
||||
module.exports = createWebSocketServer;
|
||||
433
backend/api.js
433
backend/api.js
@@ -1,42 +1,40 @@
|
||||
// api.js
|
||||
const express = require('express');
|
||||
const pgp = require('pg-promise')();
|
||||
const db = require('./db');
|
||||
const express = require("express");
|
||||
// const pgp = require("pg-promise")();
|
||||
const db = require("./db");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/test', async (req, res) => {
|
||||
router.get("/test", async (req, res) => {
|
||||
console.log(`ENDPOINT CALLED: /test`);
|
||||
res.send('Backend Responded Succesfully');
|
||||
res.send("Backend Responded Succesfully");
|
||||
});
|
||||
|
||||
router.get('/getconfig', async (req, res) => {
|
||||
router.get("/getconfig", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
console.log(`ENDPOINT CALLED: /getconfig: ` + rows);
|
||||
// console.log(`ENDPOINT CALLED: /setconfig: `+rows.length);
|
||||
res.send(rows);
|
||||
|
||||
});
|
||||
|
||||
router.post('/setconfig', async (req, res) => {
|
||||
router.post("/setconfig", async (req, res) => {
|
||||
const { JF_HOST, JF_API_KEY } = req.body;
|
||||
|
||||
const { rows } = await db.query('UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1', [JF_HOST, JF_API_KEY]);
|
||||
const { rows } = await db.query(
|
||||
'UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1',
|
||||
[JF_HOST, JF_API_KEY]
|
||||
);
|
||||
console.log({ JF_HOST: JF_HOST, JF_API_KEY: JF_API_KEY });
|
||||
res.send(rows);
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /setconfig: `);
|
||||
|
||||
});
|
||||
|
||||
|
||||
router.get('/getAllFromJellyfin', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
router.get("/getAllFromJellyfin", async (req, res) => {
|
||||
const sync = require("./sync");
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,410 +43,7 @@ router.get('/getAllFromJellyfin', async (req, res) => {
|
||||
|
||||
res.send(results);
|
||||
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getAllFromJellyfin: `);
|
||||
|
||||
});
|
||||
router.get('/getShows', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const results = await _sync.getShows();
|
||||
|
||||
res.send(results);
|
||||
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getShows: `);
|
||||
|
||||
});
|
||||
|
||||
router.get('/writeAllShows', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const data = await _sync.getShows();
|
||||
const existingIds = await db.query('SELECT "Id" FROM jf_library_shows').then(res => res.rows.map(row => row.Id));
|
||||
|
||||
|
||||
const columns = ['Id', 'Name', 'ServerId', 'PremiereDate', 'EndDate', 'CommunityRating', 'RunTimeTicks', 'ProductionYear', 'IsFolder', 'Type', 'Status', 'ImageTagsPrimary', 'ImageTagsBanner', 'ImageTagsLogo', 'ImageTagsThumb', 'BackdropImageTags', 'ParentId']; // specify the columns to insert into
|
||||
|
||||
const dataToInsert = data.filter(row => !existingIds.includes(row.Id)).map(item => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner: item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo: item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb: item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId
|
||||
}));
|
||||
|
||||
if(!dataToInsert || dataToInsert.length==0)
|
||||
{
|
||||
res.send(('No new shows to insert'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
// const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await db.query('BEGIN');
|
||||
|
||||
const query = pgp.helpers.insert(dataToInsert, columns, 'jf_library_shows');
|
||||
await db.query(query);
|
||||
|
||||
await db.query('COMMIT');
|
||||
console.log('Bulk insert successful');
|
||||
res.send('Bulk insert successful');
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('Error performing bulk insert:', error);
|
||||
res.send(('Error performing bulk insert:', error));
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getShows: `);
|
||||
|
||||
});
|
||||
|
||||
router.get('/getSeasonsAndEpisodes', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
const results=[];
|
||||
const { rows:config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { rows:shows } = await db.query('SELECT * FROM jf_library_shows');
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
|
||||
if (shows && shows.length > 0) {
|
||||
for (const show of shows) {
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id);
|
||||
results.push(data);
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log("No shows found.");
|
||||
results.push({Status:'Error',Message:'No shows found.'});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
res.send(results);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `);
|
||||
|
||||
});
|
||||
|
||||
|
||||
router.get('/writeSeasonsAndEpisodes', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
const results=[];
|
||||
const { rows:config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { rows:shows } = await db.query('SELECT * FROM jf_library_shows');
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
|
||||
if (shows && shows.length > 0) {
|
||||
for (const show of shows) {
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id);
|
||||
const columnSeasons = ['Id', 'Name', 'ServerId', 'IndexNumber', 'Type', 'ParentLogoItemId', 'ParentBackdropItemId', 'ParentBackdropImageTags', 'SeriesName', 'SeriesId', 'SeriesPrimaryImageTag']; // specify the columns to insert into
|
||||
const columnEpisodes = ['Id', 'EpisodeId', 'Name', 'ServerId', 'PremiereDate', 'OfficialRating', 'CommunityRating', 'RunTimeTicks', 'ProductionYear', 'IndexNumber', 'ParentIndexNumber', 'Type', 'ParentLogoItemId', 'ParentBackdropItemId', 'ParentBackdropImageTags', 'SeriesId', 'SeasonId', 'SeasonName', 'SeriesName']; // specify the columns to insert into
|
||||
|
||||
const existingSeasons = await db.query('SELECT "Id" FROM jf_library_seasons').then(res => res.rows.map(row => row.Id));
|
||||
const existingEpisodes = await db.query('SELECT "Id" FROM jf_library_episodes').then(res => res.rows.map(row => row.Id));
|
||||
|
||||
const seasonsToInsert = await data.allSeasons.filter(row => !existingSeasons.includes(row.Id)).map(item => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IndexNumber: item.IndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags: item.ParentBackdropImageTags!==undefined ? item.ParentBackdropImageTags[0] : null,
|
||||
SeriesName: item.SeriesName,
|
||||
SeriesId: item.ParentId,
|
||||
SeriesPrimaryImageTag: item.SeriesPrimaryImageTag
|
||||
}));
|
||||
|
||||
const episodesToInsert =await data.allEpisodes.filter(row => !existingEpisodes.includes((row.Id+row.ParentId))).map(item => ({
|
||||
Id: (item.Id+item.ParentId),
|
||||
EpisodeId: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
OfficialRating: item.OfficialRating,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IndexNumber: item.IndexNumber,
|
||||
ParentIndexNumber: item.ParentIndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:item.ParentBackdropImageTags!==undefined ? item.ParentBackdropImageTags[0] : null,
|
||||
SeriesId: item.SeriesId,
|
||||
SeasonId: item.ParentId,
|
||||
SeasonName: item.SeasonName,
|
||||
SeriesName: item.SeriesName
|
||||
}));
|
||||
|
||||
|
||||
|
||||
|
||||
if(seasonsToInsert.length>0)
|
||||
{
|
||||
await (async () => {
|
||||
// const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await db.query('BEGIN');
|
||||
|
||||
|
||||
//insert seasons
|
||||
|
||||
const queryseasons = pgp.helpers.insert(seasonsToInsert, columnSeasons, 'jf_library_seasons');
|
||||
await db.query(queryseasons);
|
||||
|
||||
//
|
||||
|
||||
await db.query('COMMIT');
|
||||
console.log('Bulk insert successful');
|
||||
results.push({Status:'Success',Message:('Season insert successful for '+ show.Name)});
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('Error performing bulk insert:', error);
|
||||
results.push({Status:'Error',Message:('Error performing bulk insert:', error)});
|
||||
}
|
||||
})();
|
||||
}else{
|
||||
results.push({Status:'Information',Message:'No new seasons to insert for '+ show.Name});
|
||||
}
|
||||
|
||||
|
||||
|
||||
if(episodesToInsert.length>0)
|
||||
{
|
||||
await (async () => {
|
||||
// const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await db.query('BEGIN');
|
||||
|
||||
//insert episodes
|
||||
|
||||
const queryepisodes = pgp.helpers.insert(episodesToInsert, columnEpisodes, 'jf_library_episodes');
|
||||
await db.query(queryepisodes);
|
||||
|
||||
//
|
||||
|
||||
await db.query('COMMIT');
|
||||
console.log('Bulk insert successful');
|
||||
results.push({Status:'Success',Message:('Episode insert successful for '+ show.Name)});
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('Error performing bulk insert:', error);
|
||||
results.push({Status:'Error',Message:('Error performing bulk insert:', error)});
|
||||
}
|
||||
})();
|
||||
}else{
|
||||
results.push({Status:'Information',Message:'No new episodes to insert for '+ show.Name});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log("No shows found.");
|
||||
results.push({Status:'Error',Message:'No shows found.'});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
res.send(results);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.get('/writeLibraries', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const data = await _sync.getLibraries();
|
||||
|
||||
// res.send(data);
|
||||
// return;
|
||||
|
||||
const columns = ['Id', 'Name', 'ServerId', 'IsFolder', 'Type', 'CollectionType', 'ImageTagsPrimary']; // specify the columns to insert into
|
||||
|
||||
const existingIds = await db.query('SELECT "Id" FROM jf_libraries').then(res => res.rows.map(row => row.Id));
|
||||
|
||||
|
||||
const dataToInsert = data.filter(row => !existingIds.includes(row.Id)).map(item => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
CollectionType: item.CollectionType,
|
||||
ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null
|
||||
}));
|
||||
|
||||
if(dataToInsert.length===0)
|
||||
{
|
||||
res.send('No new libraries to insert');
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await db.query('BEGIN');
|
||||
|
||||
const query = pgp.helpers.insert(dataToInsert, columns, 'jf_libraries');
|
||||
await db.query(query);
|
||||
|
||||
await db.query('COMMIT');
|
||||
console.log('Bulk insert successful');
|
||||
res.send('Bulk insert successful');
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('Error performing bulk insert:', error);
|
||||
res.send(('Error performing bulk insert:', error));
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// res.send(results);
|
||||
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraries: `);
|
||||
|
||||
});
|
||||
|
||||
router.get('/writeLibraryItems', async (req, res) => {
|
||||
|
||||
const sync = require('./sync');
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: 'Config Details Not Found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const data = await _sync.getAllItems();
|
||||
const existingIds = await db.query('SELECT "Id" FROM jf_library_items').then(res => res.rows.map(row => row.Id));
|
||||
|
||||
// res.send(data);
|
||||
// return;
|
||||
|
||||
const columns = ['Id', 'Name', 'ServerId', 'PremiereDate', 'EndDate', 'CommunityRating', 'RunTimeTicks', 'ProductionYear', 'IsFolder', 'Type', 'Status', 'ImageTagsPrimary', 'ImageTagsBanner', 'ImageTagsLogo', 'ImageTagsThumb', 'BackdropImageTags', 'ParentId']; // specify the columns to insert into
|
||||
|
||||
const dataToInsert = data.filter(row => !existingIds.includes(row.Id)).map(item => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner: item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo: item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb: item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId
|
||||
}));
|
||||
|
||||
if(dataToInsert.length===0)
|
||||
{
|
||||
res.send('No new library Items to insert');
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await db.query('BEGIN');
|
||||
await db.query('TRUNCATE TABLE jf_library_items');
|
||||
const query = pgp.helpers.insert(dataToInsert, columns, 'jf_library_items');
|
||||
await db.query(query);
|
||||
|
||||
await db.query('COMMIT');
|
||||
console.log('Bulk insert successful');
|
||||
res.send('Bulk insert successful');
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('Error performing bulk insert:', error);
|
||||
res.send(('Error performing bulk insert:', error));
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// res.send(results);
|
||||
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraryItems: `);
|
||||
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const apiRouter = require('./api');
|
||||
const syncRouter = require('./sync');
|
||||
const statsRouter = require('./stats');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3003;
|
||||
@@ -10,6 +12,8 @@ const LISTEN_IP = '127.0.0.1';
|
||||
app.use(express.json()); // middleware to parse JSON request bodies
|
||||
app.use(cors());
|
||||
app.use('/api', apiRouter); // mount the API router at /api
|
||||
app.use('/sync', syncRouter); // mount the API router at /sync
|
||||
app.use('/stats', statsRouter); // mount the API router at /stats
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server listening on http://${LISTEN_IP}:${PORT}`);
|
||||
|
||||
18
backend/stats.js
Normal file
18
backend/stats.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// api.js
|
||||
const express = require("express");
|
||||
const db = require("./db");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/test", async (req, res) => {
|
||||
console.log(`ENDPOINT CALLED: /test`);
|
||||
res.send("Backend Responded Succesfully");
|
||||
});
|
||||
|
||||
router.get("/getLibraryOverview", async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM jf_library_count_view');
|
||||
res.send(rows);
|
||||
console.log(`ENDPOINT CALLED: /getLibraryOverview`);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
778
backend/sync.js
778
backend/sync.js
@@ -1,25 +1,34 @@
|
||||
// import { Component } from 'react';
|
||||
const axios = require('axios');
|
||||
// import Config from '../lib/config';
|
||||
const express = require("express");
|
||||
const pgp = require("pg-promise")();
|
||||
const db = require("./db");
|
||||
const axios = require("axios");
|
||||
|
||||
class sync {
|
||||
const ws = require("./WebsocketHandler");
|
||||
const sendMessageToClients = ws(8080);
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////Functions
|
||||
class sync {
|
||||
constructor(hostUrl, apiKey) {
|
||||
this.hostUrl = hostUrl;
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
|
||||
async getAdminUser() {
|
||||
|
||||
try {
|
||||
const url = `${this.hostUrl}/Users`;
|
||||
console.log('getAdminUser: ',url);
|
||||
console.log("getAdminUser: ", url);
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': this.apiKey,
|
||||
"X-MediaBrowser-Token": this.apiKey,
|
||||
},
|
||||
});
|
||||
const adminUser = response.data.filter(user => user.Policy.IsAdministrator === true);
|
||||
const adminUser = response.data.filter(
|
||||
(user) => user.Policy.IsAdministrator === true
|
||||
);
|
||||
return adminUser || null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -27,106 +36,697 @@ class sync {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllItems() {
|
||||
if( this.hostUrl ===undefined || this.apiKey ===undefined)
|
||||
{
|
||||
return('Error: Method Not Initialized');
|
||||
}
|
||||
const libraries=await this.getLibraries();
|
||||
const allitems=[];
|
||||
for (let i = 0; i < libraries.length; i++) {
|
||||
const item = libraries[i];
|
||||
let libraryItems=await this.getItem(item.Id);
|
||||
// allitems.push({library:item, data:libraryItems});
|
||||
const libraryItemsWithParent = libraryItems.map(items => ({ ...items, ...{ParentId: item.Id} }));
|
||||
// libraryItems["ParentId"]=item.Id;
|
||||
allitems.push(...libraryItemsWithParent);
|
||||
|
||||
}
|
||||
return allitems;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async getLibraries() {
|
||||
|
||||
try {
|
||||
|
||||
const admins=await this.getAdminUser()
|
||||
const userid=admins[0].Id;
|
||||
const url = `${this.hostUrl}/users/${userid}/Items`;
|
||||
console.log('getLibraries: ',url);
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': this.apiKey,
|
||||
},
|
||||
});
|
||||
const mediafolders = response.data.Items.filter(type => ['tvshows','movies'].includes(type.CollectionType));
|
||||
return mediafolders || null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getItem(itemID) {
|
||||
|
||||
try {
|
||||
|
||||
const admins=await this.getAdminUser()
|
||||
const userid=admins[0].Id;
|
||||
const url = `${this.hostUrl}/users/${userid}/Items?ParentID=${itemID}`;
|
||||
const admins = await this.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
let url = `${this.hostUrl}/users/${userid}/Items`;
|
||||
if (itemID !== undefined) {
|
||||
url += `?ParentID=${itemID}`;
|
||||
}
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': this.apiKey,
|
||||
"X-MediaBrowser-Token": this.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data.Items;
|
||||
|
||||
const results = response.data.Items;
|
||||
if (itemID === undefined) {
|
||||
return results.filter((type) =>
|
||||
["tvshows", "movies"].includes(type.CollectionType)
|
||||
);
|
||||
} else {
|
||||
return results;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async getSeasonsAndEpisodes(showId) {
|
||||
const allSeasons = [];
|
||||
const allEpisodes = [];
|
||||
|
||||
async getShows() {
|
||||
|
||||
let libraries=await this.getLibraries();
|
||||
libraries=libraries.filter(type => type.CollectionType==='tvshows');
|
||||
const allShows=[];
|
||||
for (let i = 0; i < libraries.length; i++) {
|
||||
const item = libraries[i];
|
||||
const showItems=await this.getItem(item.Id);
|
||||
const showsWithParent = showItems.map(items => ({ ...items, ...{ParentId: item.Id} }));
|
||||
allShows.push(...showsWithParent);
|
||||
}
|
||||
return allShows;
|
||||
let seasonItems = await this.getItem(showId);
|
||||
const seasonWithParent = seasonItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: showId },
|
||||
}));
|
||||
allSeasons.push(...seasonWithParent);
|
||||
for (let e = 0; e < seasonItems.length; e++) {
|
||||
const season = seasonItems[e];
|
||||
let episodeItems = await this.getItem(season.Id);
|
||||
const episodeWithParent = episodeItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: season.Id },
|
||||
}));
|
||||
allEpisodes.push(...episodeWithParent);
|
||||
}
|
||||
|
||||
return { allSeasons: allSeasons, allEpisodes: allEpisodes };
|
||||
}
|
||||
}
|
||||
////////////////////////////////////////API Methods
|
||||
|
||||
///////////////////////////////////////writeLibraries
|
||||
router.get("/writeLibraries", async (req, res) => {
|
||||
let message = [];
|
||||
|
||||
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
sendMessageToClients({Message:"Error: Config details not found!" });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
|
||||
const data = await _sync.getItem(); //getting all root folders aka libraries
|
||||
|
||||
async getSeasonsAndEpisodes(showId) {
|
||||
|
||||
const allSeasons=[];
|
||||
const allEpisodes=[];
|
||||
const columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"CollectionType",
|
||||
"ImageTagsPrimary",
|
||||
]; // specify the columns to insert into
|
||||
|
||||
const existingIds = await db
|
||||
.query('SELECT "Id" FROM jf_libraries')
|
||||
.then((res) => res.rows.map((row) => row.Id)); // get existing library Ids from the db
|
||||
|
||||
//data mapping
|
||||
const mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
CollectionType: item.CollectionType,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
});
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
dataToInsert = await data.map(mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
dataToInsert = await data
|
||||
.filter((row) => !existingIds.includes(row.Id))
|
||||
.map(mapping);
|
||||
}
|
||||
|
||||
//Bulkinsert new data not on db
|
||||
if (dataToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(dataToInsert, columns, "jf_libraries");
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: dataToInsert.length + " Rows Inserted.",
|
||||
});
|
||||
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
sendMessageToClients({Message:"Error performing bulk insert:" + error});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk insert" });
|
||||
sendMessageToClients({Message:"No new data to bulk insert"});
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIds.length > data.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const AllIds = data.map((row) => row.Id);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_libraries WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: existingIds.length - data.length + " Rows Removed.",
|
||||
});
|
||||
sendMessageToClients(existingIds.length - data.length + " Rows Removed.");
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
sendMessageToClients({Message:"Error performing bulk removal:" + error});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk delete" });
|
||||
sendMessageToClients({Message:"No new data to bulk delete"});
|
||||
}
|
||||
//Sent logs
|
||||
|
||||
|
||||
let seasonItems=await this.getItem(showId);
|
||||
const seasonWithParent = seasonItems.map(items => ({ ...items, ...{ParentId: showId} }));
|
||||
allSeasons.push(...seasonWithParent);
|
||||
for (let e = 0; e < seasonItems.length; e++) {
|
||||
const season = seasonItems[e];
|
||||
let episodeItems=await this.getItem(season.Id);
|
||||
const episodeWithParent = episodeItems.map(items => ({ ...items, ...{ParentId: season.Id} }));
|
||||
allEpisodes.push(...episodeWithParent);
|
||||
res.send(message);
|
||||
|
||||
}
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraries: `);
|
||||
});
|
||||
|
||||
return {allSeasons:allSeasons,allEpisodes:allEpisodes};
|
||||
}
|
||||
//////////////////////////////////////////////////////writeLibraryItems
|
||||
router.get("/writeLibraryItems", async (req, res) => {
|
||||
let message = [];
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
|
||||
//Get all Library items
|
||||
//gets all libraries
|
||||
const libraries = await _sync.getItem();
|
||||
const data = [];
|
||||
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
|
||||
for (let i = 0; i < libraries.length; i++) {
|
||||
const item = libraries[i];
|
||||
let libraryItems = await _sync.getItem(item.Id);
|
||||
const libraryItemsWithParent = libraryItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: item.Id },
|
||||
}));
|
||||
data.push(...libraryItemsWithParent);
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
|
||||
module.exports= sync;
|
||||
const existingIds = await db
|
||||
.query('SELECT "Id" FROM jf_library_items')
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
//Mappings to store data in DB
|
||||
const columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"EndDate",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"Status",
|
||||
"ImageTagsPrimary",
|
||||
"ImageTagsBanner",
|
||||
"ImageTagsLogo",
|
||||
"ImageTagsThumb",
|
||||
"BackdropImageTags",
|
||||
"ParentId",
|
||||
]; // specify the columns to insert into
|
||||
|
||||
//data mapping
|
||||
const mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner:
|
||||
item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo:
|
||||
item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb:
|
||||
item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId,
|
||||
});
|
||||
let dataToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIds.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
dataToInsert = await data.map(mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
dataToInsert = await data
|
||||
.filter((row) => !existingIds.includes(row.Id))
|
||||
.map(mapping);
|
||||
}
|
||||
|
||||
//Bulkinsert new data not on db
|
||||
if (dataToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
dataToInsert,
|
||||
columns,
|
||||
"jf_library_items"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: dataToInsert.length + " Rows Inserted.",
|
||||
});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk insert" });
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIds.length > data.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const AllIds = data.map((row) => row.Id);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_library_items WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: existingIds.length - data.length + " Rows Removed.",
|
||||
});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({ Type: "Success", Message: "No new data to bulk delete" });
|
||||
}
|
||||
//Sent logs
|
||||
|
||||
res.send(message);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeLibraryItems: `);
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////writeSeasonsAndEpisodes
|
||||
router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
sendMessageToClients({color:'yellow',Message:"Beginning Seasons and Episode sync"});
|
||||
const message = [];
|
||||
const { rows: config } = await db.query(
|
||||
'SELECT * FROM app_config where "ID"=1'
|
||||
);
|
||||
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
|
||||
res.send({ error: "Config Details Not Found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
const { rows: shows } = await db.query(
|
||||
`SELECT * FROM public.jf_library_items where "Type"='Series'`
|
||||
);
|
||||
|
||||
//loop for each show
|
||||
for (const show of shows) {
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id);
|
||||
|
||||
//
|
||||
//get existing seasons and episodes
|
||||
console.log(show.Id);
|
||||
const existingIdsSeasons = await db
|
||||
.query(
|
||||
`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`
|
||||
)
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
let existingIdsEpisodes = [];
|
||||
if (existingIdsSeasons.length > 0) {
|
||||
existingIdsEpisodes = await db
|
||||
.query(
|
||||
`SELECT * FROM public.jf_library_episodes WHERE "SeasonId" IN (${existingIdsSeasons
|
||||
.filter((seasons) => seasons !== "")
|
||||
.map((seasons) => pgp.as.value(seasons))
|
||||
.map((value) => "'" + value + "'")
|
||||
.join(", ")})`
|
||||
)
|
||||
.then((res) => res.rows.map((row) => row.Id));
|
||||
}
|
||||
|
||||
//Mappings to store data in DB
|
||||
const columnSeasons = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"IndexNumber",
|
||||
"Type",
|
||||
"ParentLogoItemId",
|
||||
"ParentBackdropItemId",
|
||||
"ParentBackdropImageTags",
|
||||
"SeriesName",
|
||||
"SeriesId",
|
||||
"SeriesPrimaryImageTag",
|
||||
]; // specify the columns to insert into
|
||||
const columnEpisodes = [
|
||||
"Id",
|
||||
"EpisodeId",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"OfficialRating",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IndexNumber",
|
||||
"ParentIndexNumber",
|
||||
"Type",
|
||||
"ParentLogoItemId",
|
||||
"ParentBackdropItemId",
|
||||
"ParentBackdropImageTags",
|
||||
"SeriesId",
|
||||
"SeasonId",
|
||||
"SeasonName",
|
||||
"SeriesName",
|
||||
]; // specify the columns to insert into
|
||||
|
||||
//data mapping
|
||||
const seasonsmapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
IndexNumber: item.IndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:
|
||||
item.ParentBackdropImageTags !== undefined
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesName: item.SeriesName,
|
||||
SeriesId: item.ParentId,
|
||||
SeriesPrimaryImageTag: item.SeriesPrimaryImageTag,
|
||||
});
|
||||
|
||||
const episodemapping = (item) => ({
|
||||
Id: item.Id + item.ParentId,
|
||||
EpisodeId: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
OfficialRating: item.OfficialRating,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IndexNumber: item.IndexNumber,
|
||||
ParentIndexNumber: item.ParentIndexNumber,
|
||||
Type: item.Type,
|
||||
ParentLogoItemId: item.ParentLogoItemId,
|
||||
ParentBackdropItemId: item.ParentBackdropItemId,
|
||||
ParentBackdropImageTags:
|
||||
item.ParentBackdropImageTags !== undefined
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesId: item.SeriesId,
|
||||
SeasonId: item.ParentId,
|
||||
SeasonName: item.SeasonName,
|
||||
SeriesName: item.SeriesName,
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
let seasonsToInsert = [];
|
||||
let episodesToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingIdsSeasons.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
seasonsToInsert = await data.allSeasons.map(seasonsmapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
seasonsToInsert = await data.allSeasons
|
||||
.filter((row) => !existingIdsSeasons.includes(row.Id))
|
||||
.map(seasonsmapping);
|
||||
}
|
||||
|
||||
if (existingIdsEpisodes.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
episodesToInsert = await data.allEpisodes.map(episodemapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
episodesToInsert = await data.allEpisodes
|
||||
.filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId))
|
||||
.map(episodemapping);
|
||||
}
|
||||
|
||||
///insert delete seasons
|
||||
//Bulkinsert new data not on db
|
||||
if (seasonsToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
seasonsToInsert,
|
||||
columnSeasons,
|
||||
"jf_library_seasons"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: seasonsToInsert.length + " Rows Inserted for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'cornflowerblue',Message:seasonsToInsert.length + " Rows Inserted for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk insert for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk insert for " + show.Name});
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIdsSeasons.length > data.allSeasons.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const AllIds = data.allSeasons.map((row) => row.Id);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_library_seasons WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message:
|
||||
existingIdsSeasons.length -
|
||||
data.allSeasons.length +
|
||||
" Rows Removed for " +
|
||||
show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'orange',Message:existingIdsSeasons.length -data.allSeasons.length +" Rows Removed for " +show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk delete for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_seasons",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
}
|
||||
//insert delete episodes
|
||||
//Bulkinsert new data not on db
|
||||
if (episodesToInsert.length !== 0) {
|
||||
//insert new
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const query = pgp.helpers.insert(
|
||||
episodesToInsert,
|
||||
columnEpisodes,
|
||||
"jf_library_episodes"
|
||||
);
|
||||
await db.query(query);
|
||||
|
||||
await db.query("COMMIT");
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message:
|
||||
episodesToInsert.length + " Rows Inserted for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'cornflowerblue',Message:episodesToInsert.length + " Rows Inserted for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk insert:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk insert:" + error});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk insert for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk insert for " + show.Name});
|
||||
}
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (existingIdsEpisodes.length > data.allEpisodes.length) {
|
||||
await (async () => {
|
||||
try {
|
||||
await db.query("BEGIN");
|
||||
|
||||
const AllIds = data.allEpisodes.map((row) => row.Id + row.ParentId);
|
||||
|
||||
const deleteQuery = {
|
||||
text: `DELETE FROM jf_library_episodes WHERE "Id" NOT IN (${pgp.as.csv(
|
||||
AllIds
|
||||
)})`,
|
||||
};
|
||||
const queries = [deleteQuery];
|
||||
for (let query of queries) {
|
||||
await db.query(query);
|
||||
}
|
||||
|
||||
await db.query("COMMIT");
|
||||
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message:
|
||||
existingIdsEpisodes.length -
|
||||
data.allEpisodes.length +
|
||||
" Rows Removed for " +
|
||||
show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'orange',Message: existingIdsEpisodes.length - data.allEpisodes.length + " Rows Removed for " + show.Name});
|
||||
} catch (error) {
|
||||
await db.query("ROLLBACK");
|
||||
|
||||
message.push({
|
||||
Type: "Error",
|
||||
Message: "Error performing bulk removal:" + error,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({color:'red',Message:"Error performing bulk removal:" + error});
|
||||
}
|
||||
})();
|
||||
} else {
|
||||
message.push({
|
||||
Type: "Success",
|
||||
Message: "No new data to bulk delete for " + show.Name,
|
||||
ItemId: show.Id,
|
||||
TableName: "jf_library_episodes",
|
||||
});
|
||||
sendMessageToClients({Message:"No new data to bulk delete for " + show.Name});
|
||||
}
|
||||
}
|
||||
sendMessageToClients({color:'lightgreen',Message:"Sync Complete"});
|
||||
res.send(message);
|
||||
|
||||
console.log(`ENDPOINT CALLED: /writeSeasonsAndEpisodes: `);
|
||||
});
|
||||
|
||||
//////////////////////////////////////
|
||||
|
||||
module.exports = router;
|
||||
|
||||
73
package-lock.json
generated
73
package-lock.json
generated
@@ -27,7 +27,8 @@
|
||||
"remixicon-react": "^1.0.0",
|
||||
"sequelize": "^6.29.0",
|
||||
"sqlite3": "^5.1.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
@@ -12885,6 +12886,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
@@ -19263,26 +19284,6 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ws": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",
|
||||
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-manifest-plugin": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz",
|
||||
@@ -19857,15 +19858,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
@@ -29181,6 +29182,14 @@
|
||||
"whatwg-url": "^8.5.0",
|
||||
"ws": "^7.4.6",
|
||||
"xml-name-validator": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsesc": {
|
||||
@@ -33647,12 +33656,6 @@
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz",
|
||||
"integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -34124,9 +34127,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml-name-validator": {
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"remixicon-react": "^1.0.0",
|
||||
"sequelize": "^6.29.0",
|
||||
"sqlite3": "^5.1.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
"web-vitals": "^2.1.4",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
import { Component } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
class API extends Component {
|
||||
constructor(props) {
|
||||
@@ -10,14 +10,13 @@ class API extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async getSessions() {
|
||||
try {
|
||||
const config = await Config();
|
||||
const url = `${config.hostUrl}/Sessions`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
@@ -28,16 +27,15 @@ class API extends Component {
|
||||
}
|
||||
|
||||
async getActivityData(limit) {
|
||||
if(limit===undefined || limit<1)
|
||||
{
|
||||
return[];
|
||||
if (limit === undefined || limit < 1) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const config = await Config();
|
||||
const url = `${config.hostUrl}/System/ActivityLog/Entries?limit=${limit}`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
@@ -53,10 +51,12 @@ class API extends Component {
|
||||
const url = `${config.hostUrl}/Users`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
const adminUser = response.data.filter(user => user.Policy.IsAdministrator === true);
|
||||
const adminUser = response.data.filter(
|
||||
(user) => user.Policy.IsAdministrator === true
|
||||
);
|
||||
return adminUser || null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -65,35 +65,35 @@ class API extends Component {
|
||||
}
|
||||
|
||||
async getLibraries() {
|
||||
|
||||
try {
|
||||
const config = await Config();
|
||||
const admins=await this.getAdminUser()
|
||||
const userid=admins[0].Id;
|
||||
const admins = await this.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
const url = `${config.hostUrl}/users/${userid}/Items`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
const mediafolders = response.data.Items.filter(type => ['tvshows','movies'].includes(type.CollectionType));
|
||||
const mediafolders = response.data.Items.filter((type) =>
|
||||
["tvshows", "movies"].includes(type.CollectionType)
|
||||
);
|
||||
return mediafolders || null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getItem(itemID) {
|
||||
|
||||
try {
|
||||
const config = await Config();
|
||||
const admins=await this.getAdminUser()
|
||||
const userid=admins[0].Id;
|
||||
const admins = await this.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
const url = `${config.hostUrl}/users/${userid}/Items?ParentID=${itemID}`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data.Items;
|
||||
@@ -103,14 +103,13 @@ class API extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async getRecentlyPlayed(userid,limit) {
|
||||
|
||||
async getRecentlyPlayed(userid, limit) {
|
||||
try {
|
||||
const config = await Config();
|
||||
const url = `${config.hostUrl}/users/${userid}/Items/Resume?limit=${limit}`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
"X-MediaBrowser-Token": config.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data.Items;
|
||||
@@ -119,8 +118,6 @@ class API extends Component {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default API;
|
||||
|
||||
@@ -1,50 +1,44 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import API from '../classes/jellyfin-api';
|
||||
|
||||
import '../App.css'
|
||||
|
||||
import Loading from './components/loading';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import API from "../classes/jellyfin-api";
|
||||
|
||||
import "../App.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
function Activity() {
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let _api= new API()
|
||||
let _api = new API();
|
||||
|
||||
const fetchData = () => {
|
||||
_api.getActivityData(30).then((ActivityData) => {
|
||||
if (data && data.length > 0)
|
||||
{
|
||||
const newDataOnly = ActivityData.Items.filter(item => {
|
||||
return !data.some(existingItem => existingItem.Id === item.Id);
|
||||
if (data && data.length > 0) {
|
||||
const newDataOnly = ActivityData.Items.filter((item) => {
|
||||
return !data.some((existingItem) => existingItem.Id === item.Id);
|
||||
});
|
||||
setData([...newDataOnly, ...data.slice(0, data.length - newDataOnly.length)]);
|
||||
} else
|
||||
{
|
||||
setData([
|
||||
...newDataOnly,
|
||||
...data.slice(0, data.length - newDataOnly.length),
|
||||
]);
|
||||
} else {
|
||||
setData(ActivityData.Items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const options = {
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: false
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
@@ -52,16 +46,25 @@ function Activity() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='Activity'>
|
||||
<div className="Activity">
|
||||
<h1>Activity Log</h1>
|
||||
<ul>
|
||||
{data &&
|
||||
data.map(item => (
|
||||
<li key={item.Id} className={data.findIndex(items => items.Id === item.Id) <= 30 ? 'new' : 'old'}>
|
||||
|
||||
<div className='ActivityDetail'> {item.Name}</div>
|
||||
<div className='ActivityTime'>{new Date(item.Date).toLocaleString('en-GB', options).replace(',', '')}</div>
|
||||
|
||||
data.map((item) => (
|
||||
<li
|
||||
key={item.Id}
|
||||
className={
|
||||
data.findIndex((items) => items.Id === item.Id) <= 30
|
||||
? "new"
|
||||
: "old"
|
||||
}
|
||||
>
|
||||
<div className="ActivityDetail"> {item.Name}</div>
|
||||
<div className="ActivityTime">
|
||||
{new Date(item.Date)
|
||||
.toLocaleString("en-GB", options)
|
||||
.replace(",", "")}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -70,4 +73,3 @@ function Activity() {
|
||||
}
|
||||
|
||||
export default Activity;
|
||||
|
||||
|
||||
@@ -1,60 +1,76 @@
|
||||
import "../css/libraryOverview.css"
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from "axios";
|
||||
import "../css/libraryOverview.css";
|
||||
import Config from "../../lib/config";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Loading from "./loading";
|
||||
|
||||
export default function LibraryOverView() {
|
||||
const [data, setData] = useState([]);
|
||||
const [data, setData] = useState([]);
|
||||
const [base_url, setURL] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (base_url === "") {
|
||||
Config()
|
||||
.then((config) => {
|
||||
setURL(config.hostUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
const fetchData = () => {
|
||||
const url = `http://localhost:3003/stats/getLibraryOverview`;
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => setData(response.data))
|
||||
.catch((error) => console.log(error));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Config().then(config => {
|
||||
const url = `${config.hostUrl}/Items/counts`;
|
||||
const fetchData = () => {
|
||||
axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
},
|
||||
})
|
||||
.then(response => setData(response.data))
|
||||
.catch(error => console.log(error));
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 60000);
|
||||
return () => clearInterval(intervalId);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Loading />;
|
||||
if (!data || data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
}, [data,base_url]);
|
||||
|
||||
return (
|
||||
<div className="overview">
|
||||
<div className="card">
|
||||
<div className="item-card-count">
|
||||
{data.MovieCount + data.EpisodeCount} Media Files
|
||||
</div>
|
||||
if (data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overview">
|
||||
{data &&
|
||||
data.map((stats) => (
|
||||
<div className="card" style={{
|
||||
backgroundImage: `url(${
|
||||
base_url +
|
||||
"/Items/" +
|
||||
(stats.Isd) +
|
||||
"/Images/Primary?quality=50"
|
||||
})`,
|
||||
}}
|
||||
key={stats.Id}
|
||||
>
|
||||
<div className="item-count">
|
||||
<div>
|
||||
<p>Items in Library</p><p>{stats.Library_Count}</p>
|
||||
</div>
|
||||
{stats.CollectionType === "tvshows" ? (
|
||||
<div>
|
||||
<div className="item-count">
|
||||
<p>{data.MovieCount}</p> <p>Movies</p>
|
||||
</div>
|
||||
<div className="item-count">
|
||||
<p>{data.BoxSetCount}</p> <p>Box Sets</p>
|
||||
</div>
|
||||
<div className="item-count">
|
||||
<p>{data.SeriesCount}</p> <p>Shows</p>
|
||||
</div>
|
||||
<div className="item-count">
|
||||
<p>{data.EpisodeCount}</p> <p>Episodes</p>
|
||||
</div>
|
||||
<p>Seasons</p><p>{stats.Season_Count}</p>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{stats.CollectionType === "tvshows" ? (
|
||||
<div>
|
||||
<p>Episodes</p><p>{stats.Episode_Count}</p>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,76 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
function getLastPlayedTimeString(datetime) {
|
||||
const now = new Date();
|
||||
const lastPlayed = new Date(datetime);
|
||||
|
||||
const timeDifference = Math.abs(now.getTime() - lastPlayed.getTime());
|
||||
const yearsDifference = Math.floor(timeDifference / (1000 * 3600 * 24 * 365));
|
||||
const weeksDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600 * 24 * 365)) / (1000 * 3600 * 24 * 7)
|
||||
);
|
||||
const daysDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600 * 24 * 7)) / (1000 * 3600 * 24)
|
||||
);
|
||||
const hoursDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600 * 24)) / (1000 * 3600)
|
||||
);
|
||||
const minutesDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600)) / (1000 * 60)
|
||||
);
|
||||
|
||||
const timeUnits = [
|
||||
{ label: "year", pluralLabel: "years", value: yearsDifference },
|
||||
{ label: "week", pluralLabel: "weeks", value: weeksDifference },
|
||||
{ label: "day", pluralLabel: "days", value: daysDifference },
|
||||
{ label: "hour", pluralLabel: "hours", value: hoursDifference },
|
||||
{ label: "minute", pluralLabel: "minutes", value: minutesDifference },
|
||||
];
|
||||
|
||||
const timeString = timeUnits
|
||||
.filter((unit) => unit.value > 0)
|
||||
.map((unit, index, array) => {
|
||||
const label = unit.value === 1 ? unit.label : unit.pluralLabel;
|
||||
if (index === array.length - 1 && array.length > 1) {
|
||||
// Special case for last time unit
|
||||
return `and ${unit.value} ${label}`;
|
||||
} else {
|
||||
return `${unit.value} ${label}`;
|
||||
}
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return `Watched ${timeString} ago`;
|
||||
}
|
||||
const now = new Date();
|
||||
const lastPlayed = new Date(datetime);
|
||||
|
||||
const timeDifference = Math.abs(now.getTime() - lastPlayed.getTime());
|
||||
const yearsDifference = Math.floor(timeDifference / (1000 * 3600 * 24 * 365));
|
||||
const weeksDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600 * 24 * 365)) / (1000 * 3600 * 24 * 7)
|
||||
);
|
||||
const daysDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600 * 24 * 7)) / (1000 * 3600 * 24)
|
||||
);
|
||||
const hoursDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600 * 24)) / (1000 * 3600)
|
||||
);
|
||||
const minutesDifference = Math.floor(
|
||||
(timeDifference % (1000 * 3600)) / (1000 * 60)
|
||||
);
|
||||
|
||||
const timeUnits = [
|
||||
{ label: "year", pluralLabel: "years", value: yearsDifference },
|
||||
{ label: "week", pluralLabel: "weeks", value: weeksDifference },
|
||||
{ label: "day", pluralLabel: "days", value: daysDifference },
|
||||
{ label: "hour", pluralLabel: "hours", value: hoursDifference },
|
||||
{ label: "minute", pluralLabel: "minutes", value: minutesDifference },
|
||||
];
|
||||
|
||||
const timeString = timeUnits
|
||||
.filter((unit) => unit.value > 0)
|
||||
.map((unit, index, array) => {
|
||||
const label = unit.value === 1 ? unit.label : unit.pluralLabel;
|
||||
if (index === array.length - 1 && array.length > 1) {
|
||||
// Special case for last time unit
|
||||
return `and ${unit.value} ${label}`;
|
||||
} else {
|
||||
return `${unit.value} ${label}`;
|
||||
}
|
||||
})
|
||||
.join(" ");
|
||||
|
||||
return `Watched ${timeString} ago`;
|
||||
}
|
||||
|
||||
function RecentCard(props) {
|
||||
|
||||
|
||||
return (
|
||||
<div key={props.data.recent.Id} className='recent-card' >
|
||||
<div key={props.data.recent.Id} className="recent-card">
|
||||
<div
|
||||
className="recent-card-banner"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
props.data.base_url +
|
||||
"/Items/" +
|
||||
(props.data.recent.SeriesId
|
||||
? props.data.recent.SeriesId
|
||||
: props.data.recent.Id) +
|
||||
"/Images/Primary?quality=50&tag=" +
|
||||
props.data.recent.SeriesPrimaryImageTag ||
|
||||
props.data.recent.ImageTags.Primary
|
||||
})`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
|
||||
<div className='recent-card-banner'
|
||||
style={{ backgroundImage: `url(${props.data.base_url + '/Items/' + (props.data.recent.SeriesId ? props.data.recent.SeriesId : props.data.recent.Id) + '/Images/Primary?quality=50&tag=' + props.data.recent.SeriesPrimaryImageTag || props.data.recent.ImageTags.Primary})` }}
|
||||
>
|
||||
<div className="recent-card-details">
|
||||
<div className="recent-card-item-name"> {props.data.recent.Name}</div>
|
||||
<div className="recent-card-last-played">
|
||||
{" "}
|
||||
{getLastPlayedTimeString(props.data.recent.UserData.LastPlayedDate)}
|
||||
</div>
|
||||
|
||||
<div className='recent-card-details' >
|
||||
<div className='recent-card-item-name'> {props.data.recent.Name}</div>
|
||||
<div className='recent-card-last-played'> {getLastPlayedTimeString(props.data.recent.UserData.LastPlayedDate)}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecentCard;
|
||||
export default RecentCard;
|
||||
|
||||
@@ -1,68 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from "react";
|
||||
// import axios from 'axios';
|
||||
import Config from '../../lib/config';
|
||||
import API from '../../classes/jellyfin-api';
|
||||
import Config from "../../lib/config";
|
||||
import API from "../../classes/jellyfin-api";
|
||||
|
||||
|
||||
import "../css/recent.css"
|
||||
import "../css/recent.css";
|
||||
// import "../../App.css"
|
||||
|
||||
import RecentCard from "./recent-card";
|
||||
|
||||
import RecentCard from './recent-card';
|
||||
|
||||
|
||||
import Loading from './loading';
|
||||
|
||||
|
||||
|
||||
|
||||
import Loading from "./loading";
|
||||
|
||||
function RecentlyPlayed() {
|
||||
const [data, setData] = useState([]);
|
||||
const [base_url, setURL] = useState('');
|
||||
const [base_url, setURL] = useState("");
|
||||
// const [errorHandler, seterrorHandler] = useState({ error_count: 0, error_message: '' })
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const _api = new API();
|
||||
const fetchData = () => {
|
||||
_api.getRecentlyPlayed('5f63950a2339462196eb8cead70cae7e',10).then((recentData) => {
|
||||
setData(recentData);
|
||||
});
|
||||
_api
|
||||
.getRecentlyPlayed("5f63950a2339462196eb8cead70cae7e", 10)
|
||||
.then((recentData) => {
|
||||
setData(recentData);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Config().then(config => {
|
||||
setURL(config.hostUrl);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
Config()
|
||||
.then((config) => {
|
||||
setURL(config.hostUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className='recent'>
|
||||
<div className="recent">
|
||||
{data &&
|
||||
data.sort((a, b) => b.UserData.LastPlayedDate.localeCompare(a.UserData.LastPlayedDate)).map(recent => (
|
||||
|
||||
<RecentCard data={{ recent: recent, base_url: base_url }} />
|
||||
|
||||
))}
|
||||
data
|
||||
.sort((a, b) =>
|
||||
b.UserData.LastPlayedDate.localeCompare(a.UserData.LastPlayedDate)
|
||||
)
|
||||
.map((recent) => (
|
||||
<RecentCard data={{ recent: recent, base_url: base_url }} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default RecentlyPlayed;
|
||||
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
|
||||
import AccountCircleFillIcon from 'remixicon-react/AccountCircleFillIcon';
|
||||
// import InformationFillIcon from 'remixicon-react/InformationFillIcon';
|
||||
import PlayFillIcon from 'remixicon-react/PlayFillIcon';
|
||||
import PauseFillIcon from 'remixicon-react/PauseFillIcon';
|
||||
|
||||
// import { Tooltip } from 'antd';
|
||||
|
||||
// import { Tooltip } from "@mui/material";
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
import PlayFillIcon from "remixicon-react/PlayFillIcon";
|
||||
import PauseFillIcon from "remixicon-react/PauseFillIcon";
|
||||
|
||||
function ticksToTimeString(ticks) {
|
||||
// Convert ticks to seconds
|
||||
@@ -20,7 +14,9 @@ function ticksToTimeString(ticks) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
// Format the time string as hh:MM:ss
|
||||
const timeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
const timeString = `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
|
||||
|
||||
return timeString;
|
||||
}
|
||||
@@ -29,82 +25,200 @@ function sessionCard(props) {
|
||||
// Access data passed in as a prop using `props.data`
|
||||
|
||||
if (props.data.session.NowPlayingItem === undefined) {
|
||||
return(
|
||||
<div key={props.data.session.Id} className='session-card' >
|
||||
return (
|
||||
<div key={props.data.session.Id} className="session-card">
|
||||
<div className="card-banner"></div>
|
||||
|
||||
|
||||
<div className='card-banner'>
|
||||
|
||||
</div>
|
||||
|
||||
<div className='card-details' >
|
||||
|
||||
<div className='card-device'>
|
||||
<img className='card-device-image' src={props.data.base_url + '/web/assets/img/devices/' + ((props.data.session.DeviceName.toLowerCase().includes('ios') || props.data.session.Client.toLowerCase().includes('ios')) ? 'ios' : (props.data.session.DeviceName.toLowerCase().includes('android') || props.data.session.Client.toLowerCase().includes('android')) ? 'android' : props.data.session.DeviceName.replace(' ', '').toLowerCase()) + '.svg'} alt=''></img>
|
||||
<div className='card-device-name'> {props.data.session.DeviceName}</div>
|
||||
<div className='card-client'>{props.data.session.Client + ' ' + props.data.session.ApplicationVersion}</div>
|
||||
<div className="card-details">
|
||||
<div className="card-device">
|
||||
<img
|
||||
className="card-device-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/web/assets/img/devices/" +
|
||||
(props.data.session.DeviceName.toLowerCase().includes("ios") ||
|
||||
props.data.session.Client.toLowerCase().includes("ios")
|
||||
? "ios"
|
||||
: props.data.session.DeviceName.toLowerCase().includes(
|
||||
"android"
|
||||
) ||
|
||||
props.data.session.Client.toLowerCase().includes("android")
|
||||
? "android"
|
||||
: props.data.session.DeviceName.replace(
|
||||
" ",
|
||||
""
|
||||
).toLowerCase()) +
|
||||
".svg"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
<div className="card-device-name">
|
||||
{" "}
|
||||
{props.data.session.DeviceName}
|
||||
</div>
|
||||
<div className="card-client">
|
||||
{props.data.session.Client +
|
||||
" " +
|
||||
props.data.session.ApplicationVersion}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='card-user'>
|
||||
{(props.data.session.UserPrimaryImageTag !== undefined) ? <img className='card-user-image' src={props.data.base_url + '/Users/' + props.data.session.UserId + '/Images/Primary?tag=' + props.data.session.UserPrimaryImageTag + '&quality=50'} alt='' /> : <AccountCircleFillIcon />}
|
||||
<div className='card-username'> {props.data.session.UserName}</div>
|
||||
{/* <div className='card-ip'>{props.data.session.RemoteEndPoint}</div> */}
|
||||
<div className="card-user">
|
||||
{props.data.session.UserPrimaryImageTag !== undefined ? (
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/Users/" +
|
||||
props.data.session.UserId +
|
||||
"/Images/Primary?tag=" +
|
||||
props.data.session.UserPrimaryImageTag +
|
||||
"&quality=50"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon />
|
||||
)}
|
||||
<div className="card-username"> {props.data.session.UserName}</div>
|
||||
</div>
|
||||
|
||||
<div className='card-play-state'>
|
||||
</div>
|
||||
<div className='card-item-name'> </div>
|
||||
|
||||
<div className='card-playback-position'> </div>
|
||||
<div className="card-play-state"></div>
|
||||
<div className="card-item-name"> </div>
|
||||
|
||||
<div className="card-playback-position"> </div>
|
||||
</div>
|
||||
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: `0%` }}></div>
|
||||
<div className="progress" style={{ width: `0%` }}></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// console.log(props.data);
|
||||
return (
|
||||
<div key={props.data.session.Id} className='session-card' style={{ backgroundImage: `url(${(props.data.base_url + '/Items/' + (props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id) + '/Images/Backdrop/0?maxWidth=1000&tag=' + (props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.ParentBackdropImageTags[0] : props.data.session.NowPlayingItem.BackdropImageTags[0]) + '&quality=50')})` }}>
|
||||
|
||||
|
||||
<div className='card-banner'>
|
||||
<img className='card-banner-image' src={props.data.base_url + '/Items/' + (props.data.session.NowPlayingItem.SeriesId ? props.data.session.NowPlayingItem.SeriesId : props.data.session.NowPlayingItem.Id) + '/Images/Primary?quality=50&tag=' + props.data.session.NowPlayingItem.SeriesPrimaryImageTag || props.data.session.NowPlayingItem.ImageTags.Primary} alt=''></img>
|
||||
<div
|
||||
key={props.data.session.Id}
|
||||
className="session-card"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
props.data.base_url +
|
||||
"/Items/" +
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id) +
|
||||
"/Images/Backdrop/0?maxWidth=1000&tag=" +
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.ParentBackdropImageTags[0]
|
||||
: props.data.session.NowPlayingItem.BackdropImageTags[0]) +
|
||||
"&quality=50"
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
<div className="card-banner">
|
||||
<img
|
||||
className="card-banner-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/Items/" +
|
||||
(props.data.session.NowPlayingItem.SeriesId
|
||||
? props.data.session.NowPlayingItem.SeriesId
|
||||
: props.data.session.NowPlayingItem.Id) +
|
||||
"/Images/Primary?quality=50&tag=" +
|
||||
props.data.session.NowPlayingItem.SeriesPrimaryImageTag ||
|
||||
props.data.session.NowPlayingItem.ImageTags.Primary
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
</div>
|
||||
|
||||
<div className='card-details' >
|
||||
|
||||
<div className='card-device'>
|
||||
<img className='card-device-image' src={props.data.base_url + '/web/assets/img/devices/' + ((props.data.session.DeviceName.toLowerCase().includes('ios') || props.data.session.Client.toLowerCase().includes('ios')) ? 'ios' : (props.data.session.DeviceName.toLowerCase().includes('android') || props.data.session.Client.toLowerCase().includes('android')) ? 'android' : props.data.session.DeviceName.replace(' ', '').toLowerCase()) + '.svg'} alt=''></img>
|
||||
<div className='card-device-name'> {props.data.session.DeviceName}</div>
|
||||
<div className='card-client'>{props.data.session.Client + ' ' + props.data.session.ApplicationVersion}</div>
|
||||
<div className="card-details">
|
||||
<div className="card-device">
|
||||
<img
|
||||
className="card-device-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/web/assets/img/devices/" +
|
||||
(props.data.session.DeviceName.toLowerCase().includes("ios") ||
|
||||
props.data.session.Client.toLowerCase().includes("ios")
|
||||
? "ios"
|
||||
: props.data.session.DeviceName.toLowerCase().includes(
|
||||
"android"
|
||||
) ||
|
||||
props.data.session.Client.toLowerCase().includes("android")
|
||||
? "android"
|
||||
: props.data.session.DeviceName.replace(
|
||||
" ",
|
||||
""
|
||||
).toLowerCase()) +
|
||||
".svg"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
<div className="card-device-name">
|
||||
{" "}
|
||||
{props.data.session.DeviceName}
|
||||
</div>
|
||||
<div className="card-client">
|
||||
{props.data.session.Client +
|
||||
" " +
|
||||
props.data.session.ApplicationVersion}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='card-user'>
|
||||
{(props.data.session.UserPrimaryImageTag !== undefined) ? <img className='card-user-image' src={props.data.base_url + '/Users/' + props.data.session.UserId + '/Images/Primary?tag=' + props.data.session.UserPrimaryImageTag + '&quality=50'} alt='' /> : <AccountCircleFillIcon />}
|
||||
<div className='card-username'> {props.data.session.UserName}</div>
|
||||
{/* <div className='card-ip'>{props.data.session.RemoteEndPoint}</div> */}
|
||||
<div className="card-user">
|
||||
{props.data.session.UserPrimaryImageTag !== undefined ? (
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
props.data.base_url +
|
||||
"/Users/" +
|
||||
props.data.session.UserId +
|
||||
"/Images/Primary?tag=" +
|
||||
props.data.session.UserPrimaryImageTag +
|
||||
"&quality=50"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon />
|
||||
)}
|
||||
<div className="card-username"> {props.data.session.UserName}</div>
|
||||
</div>
|
||||
|
||||
<div className='card-play-state'>{(props.data.session.PlayState.IsPaused) ? <PauseFillIcon /> : <PlayFillIcon />}
|
||||
{/* <Tooltip title={props.data.session.PlayState.PlayMethod}><InformationFillIcon color="#fff" /></Tooltip> */}
|
||||
<div className="card-play-state">
|
||||
{props.data.session.PlayState.IsPaused ? (
|
||||
<PauseFillIcon />
|
||||
) : (
|
||||
<PlayFillIcon />
|
||||
)}
|
||||
</div>
|
||||
<div className="card-item-name">
|
||||
{" "}
|
||||
{props.data.session.NowPlayingItem.Name}
|
||||
</div>
|
||||
<div className='card-item-name'> {props.data.session.NowPlayingItem.Name}</div>
|
||||
|
||||
<div className='card-playback-position'> {ticksToTimeString(props.data.session.PlayState.PositionTicks)} / {ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)}</div>
|
||||
|
||||
<div className="card-playback-position">
|
||||
{" "}
|
||||
{ticksToTimeString(props.data.session.PlayState.PositionTicks)} /{" "}
|
||||
{ticksToTimeString(props.data.session.NowPlayingItem.RunTimeTicks)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="progress-bar">
|
||||
<div className="progress" style={{ width: `${(props.data.session.PlayState.PositionTicks / props.data.session.NowPlayingItem.RunTimeTicks) * 100}%` }}></div>
|
||||
<div
|
||||
className="progress"
|
||||
style={{
|
||||
width: `${
|
||||
(props.data.session.PlayState.PositionTicks /
|
||||
props.data.session.NowPlayingItem.RunTimeTicks) *
|
||||
100
|
||||
}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default sessionCard;
|
||||
export default sessionCard;
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from "react";
|
||||
// import axios from 'axios';
|
||||
import Config from '../../lib/config';
|
||||
import API from '../../classes/jellyfin-api';
|
||||
import Config from "../../lib/config";
|
||||
import API from "../../classes/jellyfin-api";
|
||||
|
||||
import "../css/sessions.css"
|
||||
import "../css/sessions.css";
|
||||
// import "../../App.css"
|
||||
|
||||
import SessionCard from "./session-card";
|
||||
|
||||
import SessionCard from './session-card';
|
||||
|
||||
|
||||
import Loading from './loading';
|
||||
|
||||
|
||||
|
||||
|
||||
import Loading from "./loading";
|
||||
|
||||
function Sessions() {
|
||||
const [data, setData] = useState([]);
|
||||
const [base_url, setURL] = useState('');
|
||||
const [base_url, setURL] = useState("");
|
||||
// const [errorHandler, seterrorHandler] = useState({ error_count: 0, error_message: '' })
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const _api = new API();
|
||||
const fetchData = () => {
|
||||
@@ -30,41 +23,36 @@ function Sessions() {
|
||||
});
|
||||
};
|
||||
|
||||
if(base_url==='')
|
||||
{
|
||||
Config().then(config => {
|
||||
setURL(config.hostUrl);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
if (base_url === "") {
|
||||
Config()
|
||||
.then((config) => {
|
||||
setURL(config.hostUrl);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className='sessions'>
|
||||
<div className="sessions">
|
||||
{data &&
|
||||
data.sort((a, b) => a.Id.padStart(12, '0').localeCompare(b.Id.padStart(12, '0'))).map(session => (
|
||||
|
||||
<SessionCard data={{ session: session, base_url: base_url }} />
|
||||
|
||||
))}
|
||||
data
|
||||
.sort((a, b) =>
|
||||
a.Id.padStart(12, "0").localeCompare(b.Id.padStart(12, "0"))
|
||||
)
|
||||
.map((session) => (
|
||||
<SessionCard data={{ session: session, base_url: base_url }} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default Sessions;
|
||||
|
||||
|
||||
49
src/pages/components/settings/WebSocketComponent .js
Normal file
49
src/pages/components/settings/WebSocketComponent .js
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import '../../css/websocket/websocket.css';
|
||||
|
||||
const WebSocketComponent = () => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// create a new WebSocket connection
|
||||
const socket = new WebSocket('ws://localhost:8080');
|
||||
|
||||
// handle incoming messages
|
||||
socket.addEventListener('message', (event) => {
|
||||
let message = JSON.parse(event.data);
|
||||
setMessages(prevMessages => [...prevMessages, message]);
|
||||
});
|
||||
|
||||
// cleanup function to close the WebSocket connection when the component unmounts
|
||||
return () => {
|
||||
socket.close();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// function to handle scrolling to the last message
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// scroll to the last message whenever messages change
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <h1>WebSocket Example</h1> */}
|
||||
<div className="console-container">
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketComponent;
|
||||
40
src/pages/components/settings/librarySync.js
Normal file
40
src/pages/components/settings/librarySync.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import axios from "axios";
|
||||
// import Config from "../../../lib/config";
|
||||
// import Loading from "../loading";
|
||||
|
||||
import "../../css/settings.css";
|
||||
|
||||
export default function LibrarySync() {
|
||||
|
||||
async function writeSeasonsAndEpisodes() {
|
||||
// Send a GET request to /system/configuration to test copnnection
|
||||
let isValid = false;
|
||||
let errorMessage = "";
|
||||
await axios
|
||||
.get("http://localhost:3003/sync/writeSeasonsAndEpisodes")
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
writeSeasonsAndEpisodes();
|
||||
console.log('Button clicked!');
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Run Sync</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
153
src/pages/components/settings/settingsConfig.js
Normal file
153
src/pages/components/settings/settingsConfig.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../../../lib/config";
|
||||
import Loading from "../loading";
|
||||
|
||||
import "../../css/settings.css";
|
||||
|
||||
export default function SettingsConfig() {
|
||||
|
||||
// const [config, setConfig] = useState({});
|
||||
const [formValues, setFormValues] = useState({});
|
||||
const [isSubmitted, setisSubmitted] = useState("");
|
||||
const [loadSate, setloadSate] = useState("Loading");
|
||||
const [submissionMessage, setsubmissionMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
Config()
|
||||
.then((config) => {
|
||||
setFormValues({ JF_HOST: config.hostUrl, JF_API_KEY: config.apiKey });
|
||||
setloadSate("Loaded");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error updating config:", error);
|
||||
setloadSate("Critical");
|
||||
setsubmissionMessage(
|
||||
"Error Retrieving Configuration. Unable to contact Backend Server"
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function validateSettings(_url, _apikey) {
|
||||
let isValid = false;
|
||||
let errorMessage = "";
|
||||
await axios
|
||||
.get(_url + "/system/configuration", {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": _apikey,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
isValid = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// console.log(error.code);
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
isValid = false;
|
||||
errorMessage = `Error : Unable to connect to Jellyfin Server`;
|
||||
} else if (error.response.status === 401) {
|
||||
isValid = false;
|
||||
errorMessage = `Error: ${error.response.status} Not Authorized. Please check API key`;
|
||||
} else if (error.response.status === 404) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status}: The requested URL was not found.`;
|
||||
} else {
|
||||
isValid = false;
|
||||
errorMessage = `Error : ${error.response.status}`;
|
||||
}
|
||||
});
|
||||
|
||||
return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
async function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
let validation = await validateSettings(
|
||||
formValues.JF_HOST,
|
||||
formValues.JF_API_KEY
|
||||
);
|
||||
console.log(validation);
|
||||
if (!validation.isValid) {
|
||||
setisSubmitted("Failed");
|
||||
setsubmissionMessage(validation.errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
setisSubmitted("");
|
||||
|
||||
// Send a POST request to /api/setconfig/ with the updated configuration
|
||||
axios
|
||||
.post("http://localhost:3003/api/setconfig/", formValues, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
console.log("Config updated successfully:", response.data);
|
||||
setisSubmitted("Success");
|
||||
setsubmissionMessage("Success Updated Configuration");
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error updating config:", error);
|
||||
setisSubmitted("Failed");
|
||||
setsubmissionMessage("Error Updating Configuration: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFormChange(event) {
|
||||
setFormValues({ ...formValues, [event.target.name]: event.target.value });
|
||||
}
|
||||
if (loadSate === "Loading") {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (loadSate === "Critical") {
|
||||
return <div className="submit critical">{submissionMessage}</div>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="settings-form">
|
||||
<div>
|
||||
<label htmlFor="JF_HOST">Jellyfin Server</label>
|
||||
<input
|
||||
type="text"
|
||||
id="JF_HOST"
|
||||
name="JF_HOST"
|
||||
value={formValues.JF_HOST || ""}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="JF_API_KEY">API Key</label>
|
||||
<input
|
||||
type="text"
|
||||
id="JF_API_KEY"
|
||||
name="JF_API_KEY"
|
||||
value={formValues.JF_API_KEY || ""}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSubmitted !== "" ? (
|
||||
<div
|
||||
className={
|
||||
isSubmitted === "Failed" ? "submit error" : "submit success"
|
||||
}
|
||||
>
|
||||
{submissionMessage}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
@@ -1,39 +1,62 @@
|
||||
.overview{
|
||||
color: black;
|
||||
.overview {
|
||||
color: white;
|
||||
margin-top: 20px;
|
||||
font-family: 'Railway', sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card
|
||||
{
|
||||
/* width: 400px; */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
margin-right: 20px;
|
||||
height: 170px;
|
||||
background: linear-gradient(60deg, #AA5CC3 , #00A4DC);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
margin-bottom: 5px;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.item-card-count {
|
||||
|
||||
}
|
||||
|
||||
.item-card-count
|
||||
{
|
||||
font-size: 40px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.item-count
|
||||
{
|
||||
font-size: 32px;
|
||||
display: inline-block;
|
||||
.item-count {
|
||||
backdrop-filter: blur(2px);
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
p
|
||||
{
|
||||
}
|
||||
|
||||
.item-count > div {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-count > div p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-count > div:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
19
src/pages/css/websocket/websocket.css
Normal file
19
src/pages/css/websocket/websocket.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.console-container {
|
||||
background-color: black;
|
||||
color: white;
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.console-message {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.console-text {
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
@@ -1,95 +1,94 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
|
||||
|
||||
import './css/libraries.css'
|
||||
|
||||
import Loading from './components/loading';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import "./css/libraries.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
function Libraries() {
|
||||
const [data, setData] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [data, setData] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/Library/MediaFolders`;
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': apiKey,
|
||||
},
|
||||
})
|
||||
.then(data => {
|
||||
console.log('data');
|
||||
setData(data.data.Items);
|
||||
console.log(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/Library/MediaFolders`;
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": apiKey,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data.Items);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const intervalId = setInterval(fetchData, (60000*60));
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='Activity'>
|
||||
<h1>Libraries</h1>
|
||||
<ul>
|
||||
{data &&
|
||||
data.filter(collection => ['tvshows', 'movies'].includes(collection.CollectionType)).map(item => (
|
||||
<li key={item.Id}>
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
{/* <div className='ActivityDetail'> {item.Name}</div> */}
|
||||
<div className='library-banner'>
|
||||
<img className='library-banner-image' src={config.hostUrl + '/Items/' + item.Id + '/Images/Primary?quality=50'} alt=''></img>
|
||||
</div>
|
||||
const intervalId = setInterval(fetchData, 60000 * 60);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="Activity">
|
||||
<h1>Libraries</h1>
|
||||
<ul>
|
||||
{data &&
|
||||
data
|
||||
.filter((collection) =>
|
||||
["tvshows", "movies"].includes(collection.CollectionType)
|
||||
)
|
||||
.map((item) => (
|
||||
<li key={item.Id}>
|
||||
{/* <div className='ActivityDetail'> {item.Name}</div> */}
|
||||
<div className="library-banner">
|
||||
<img
|
||||
className="library-banner-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Items/" +
|
||||
item.Id +
|
||||
"/Images/Primary?quality=50"
|
||||
}
|
||||
alt=""
|
||||
></img>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Libraries;
|
||||
|
||||
|
||||
@@ -1,143 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
import Loading from './components/loading';
|
||||
import React from "react";
|
||||
|
||||
import './css/settings.css'
|
||||
import SettingsConfig from "./components/settings/settingsConfig";
|
||||
import LibrarySync from "./components/settings/librarySync";
|
||||
import WebSocketComponent from "./components/settings/WebSocketComponent ";
|
||||
|
||||
import "./css/settings.css";
|
||||
|
||||
export default function Settings() {
|
||||
// const [config, setConfig] = useState({});
|
||||
const [formValues, setFormValues] = useState({});
|
||||
const [isSubmitted, setisSubmitted] = useState('');
|
||||
const [loadSate, setloadSate] = useState('Loading');
|
||||
const [submissionMessage, setsubmissionMessage] = useState('');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
Config().then(config => {
|
||||
setFormValues({ JF_HOST: config.hostUrl, JF_API_KEY: config.apiKey });
|
||||
setloadSate('Loaded');
|
||||
}).catch(error => {
|
||||
console.log('Error updating config:', error);
|
||||
setloadSate('Critical');
|
||||
setsubmissionMessage('Error Retrieving Configuration. Unable to contact Backend Server');
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function validateSettings(_url, _apikey) {
|
||||
// Send a GET request to /system/configuration to test copnnection
|
||||
let isValid = false;
|
||||
let errorMessage = '';
|
||||
await axios.get(_url + '/system/configuration', {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': _apikey,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
// console.log('HTTP status code:', response.status); // logs the HTTP status code
|
||||
//console.log('Response data:', response.data); // logs the response data
|
||||
|
||||
if (response.status === 200) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
// console.log(error.code);
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
isValid = false;
|
||||
errorMessage = `Error : Unable to connect to Jellyfin Server`;
|
||||
} else
|
||||
if (error.response.status === 401) {
|
||||
isValid = false;
|
||||
errorMessage = `Error: ${error.response.status} Not Authorized. Please check API key`;
|
||||
} else
|
||||
if (error.response.status === 404) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status}: The requested URL was not found.`;
|
||||
} else {
|
||||
isValid = false;
|
||||
errorMessage = `Error : ${error.response.status}`;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return ({ isValid: isValid, errorMessage: errorMessage });
|
||||
}
|
||||
|
||||
async function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
let validation = await validateSettings(formValues.JF_HOST, formValues.JF_API_KEY);
|
||||
console.log(validation);
|
||||
if (!validation.isValid) {
|
||||
setisSubmitted('Failed');
|
||||
setsubmissionMessage(validation.errorMessage);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
setisSubmitted('');
|
||||
|
||||
// Send a POST request to /api/setconfig/ with the updated configuration
|
||||
axios.post('http://localhost:3003/api/setconfig/', formValues, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Config updated successfully:', response.data);
|
||||
setisSubmitted('Success');
|
||||
setsubmissionMessage('Success Updated Configuration');
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Error updating config:', error);
|
||||
setisSubmitted('Failed');
|
||||
setsubmissionMessage('Error Updating Configuration: ', error);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFormChange(event) {
|
||||
|
||||
setFormValues({ ...formValues, [event.target.name]: event.target.value });
|
||||
}
|
||||
if (loadSate === 'Loading') {
|
||||
|
||||
return (
|
||||
<Loading />
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if (loadSate === 'Critical') {
|
||||
|
||||
return (
|
||||
<div className='submit critical'>{submissionMessage}</div>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="settings-form">
|
||||
<div>
|
||||
<label htmlFor="JF_HOST">Jellyfin Server</label>
|
||||
<input type="text" id="JF_HOST" name="JF_HOST" value={formValues.JF_HOST || ''} onChange={handleFormChange} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="JF_API_KEY">API Key</label>
|
||||
<input type="text" id="JF_API_KEY" name="JF_API_KEY" value={formValues.JF_API_KEY || ''} onChange={handleFormChange} />
|
||||
</div>
|
||||
|
||||
{isSubmitted !== '' ? <div className={isSubmitted === 'Failed' ? 'submit error' : 'submit success'}>{submissionMessage}</div> :
|
||||
<></>
|
||||
}
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<SettingsConfig/>
|
||||
<LibrarySync/>
|
||||
<WebSocketComponent/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import './css/setup.css'
|
||||
import "./css/setup.css";
|
||||
|
||||
// import Loading from './components/loading';
|
||||
|
||||
@@ -10,114 +10,91 @@ function Setup() {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [formValues, setFormValues] = useState({});
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [submitButtonText, setsubmitButtonText] = useState('Save');
|
||||
const [submitButtonText, setsubmitButtonText] = useState("Save");
|
||||
|
||||
function handleFormChange(event) {
|
||||
|
||||
setFormValues({ ...formValues, [event.target.name]: event.target.value });
|
||||
}
|
||||
|
||||
async function validateSettings(_url, _apikey) {
|
||||
// Send a GET request to /system/configuration to test copnnection
|
||||
let isValid = false;
|
||||
let errorMessage = '';
|
||||
await axios.get(_url + '/system/configuration', {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': _apikey,
|
||||
},
|
||||
})
|
||||
.then(response => {
|
||||
// console.log('HTTP status code:', response.status); // logs the HTTP status code
|
||||
//console.log('Response data:', response.data); // logs the response data
|
||||
|
||||
let errorMessage = "";
|
||||
await axios
|
||||
.get(_url + "/system/configuration", {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": _apikey,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
// console.log(error.code);
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
isValid = false;
|
||||
errorMessage = `Unable to connect to Jellyfin Server`;
|
||||
} else
|
||||
if (error.response.status === 401) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status} Not Authorized`;
|
||||
} else
|
||||
if (error.response.status === 404) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status}: The requested URL was not found.`;
|
||||
} else {
|
||||
isValid = false;
|
||||
errorMessage = `Error : ${error.response.status}`;
|
||||
}
|
||||
|
||||
} else if (error.response.status === 401) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status} Not Authorized`;
|
||||
} else if (error.response.status === 404) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status}: The requested URL was not found.`;
|
||||
} else {
|
||||
isValid = false;
|
||||
errorMessage = `Error : ${error.response.status}`;
|
||||
}
|
||||
});
|
||||
|
||||
return ({ isValid: isValid, errorMessage: errorMessage });
|
||||
return { isValid: isValid, errorMessage: errorMessage };
|
||||
}
|
||||
|
||||
async function handleFormSubmit(event) {
|
||||
setProcessing(true);
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
// if(formValues.JF_HOST=='' || formValues.JF_API_KEY=='')
|
||||
// {
|
||||
// setsubmitButtonText('Plea');
|
||||
// return;
|
||||
// }
|
||||
|
||||
|
||||
let validation = await validateSettings(formValues.JF_HOST, formValues.JF_API_KEY);
|
||||
event.preventDefault();
|
||||
|
||||
let validation = await validateSettings(
|
||||
formValues.JF_HOST,
|
||||
formValues.JF_API_KEY
|
||||
);
|
||||
|
||||
if (!validation.isValid) {
|
||||
|
||||
setsubmitButtonText(validation.errorMessage);
|
||||
setProcessing(false);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Send a POST request to /api/setconfig/ with the updated configuration
|
||||
axios.post('http://localhost:3003/api/setconfig/', formValues, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
setsubmitButtonText('Settings Saved');
|
||||
axios
|
||||
.post("http://localhost:3003/api/setconfig/", formValues, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
setsubmitButtonText("Settings Saved");
|
||||
setProcessing(false);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
window.location.href = "/";
|
||||
}, 1000);
|
||||
|
||||
|
||||
return;
|
||||
})
|
||||
.catch(error => {
|
||||
setsubmitButtonText('Error Saving Settings');
|
||||
.catch((error) => {
|
||||
setsubmitButtonText("Error Saving Settings");
|
||||
setProcessing(false);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
@@ -126,32 +103,40 @@ function Setup() {
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
}, [config]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<section>
|
||||
|
||||
<div className="form-box">
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<h2>Setup</h2>
|
||||
<div className="inputbox">
|
||||
<input
|
||||
type="text"
|
||||
id="JF_HOST"
|
||||
name="JF_HOST"
|
||||
value={formValues.JF_HOST || ""}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="JF_HOST">Server URL</label>
|
||||
</div>
|
||||
<div className="inputbox">
|
||||
<input
|
||||
type="text"
|
||||
id="JF_API_KEY"
|
||||
name="JF_API_KEY"
|
||||
value={formValues.JF_API_KEY || ""}
|
||||
onChange={handleFormChange}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="JF_API_KEY">API Key</label>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleFormSubmit} >
|
||||
<h2>Setup</h2>
|
||||
<div className='inputbox'>
|
||||
<input type="text" id="JF_HOST" name="JF_HOST" value={formValues.JF_HOST || ''} onChange={handleFormChange} required/>
|
||||
<label htmlFor="JF_HOST">Server URL</label>
|
||||
|
||||
</div>
|
||||
<div className='inputbox'>
|
||||
<input type="text" id="JF_API_KEY" name="JF_API_KEY" value={formValues.JF_API_KEY || ''} onChange={handleFormChange} required/>
|
||||
<label htmlFor="JF_API_KEY">API Key</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" className='setup-button'>{processing ? 'Validating...' : submitButtonText}</button>
|
||||
</form>
|
||||
|
||||
<button type="submit" className="setup-button">
|
||||
{processing ? "Validating..." : submitButtonText}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,140 +1,138 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
import AccountCircleFillIcon from "remixicon-react/AccountCircleFillIcon";
|
||||
|
||||
import AccountCircleFillIcon from 'remixicon-react/AccountCircleFillIcon';
|
||||
|
||||
import './css/usersactivity.css'
|
||||
|
||||
import Loading from './components/loading';
|
||||
|
||||
import "./css/usersactivity.css";
|
||||
|
||||
import Loading from "./components/loading";
|
||||
|
||||
function UserActivity() {
|
||||
const [data, setData] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
|
||||
const [data, setData] = useState([]);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: null });
|
||||
|
||||
function handleSort(key) {
|
||||
const direction = sortConfig.key === key && sortConfig.direction === 'ascending' ? 'descending' : 'ascending';
|
||||
setSortConfig({ key, direction });
|
||||
}
|
||||
function handleSort(key) {
|
||||
const direction =
|
||||
sortConfig.key === key && sortConfig.direction === "ascending"
|
||||
? "descending"
|
||||
: "ascending";
|
||||
setSortConfig({ key, direction });
|
||||
}
|
||||
|
||||
function sortData(data, { key, direction }) {
|
||||
if (!key) return data;
|
||||
function sortData(data, { key, direction }) {
|
||||
if (!key) return data;
|
||||
|
||||
const sortedData = [...data];
|
||||
const sortedData = [...data];
|
||||
|
||||
sortedData.sort((a, b) => {
|
||||
if (a[key] < b[key]) return direction === 'ascending' ? -1 : 1;
|
||||
if (a[key] > b[key]) return direction === 'ascending' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
sortedData.sort((a, b) => {
|
||||
if (a[key] < b[key]) return direction === "ascending" ? -1 : 1;
|
||||
if (a[key] > b[key]) return direction === "ascending" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sortedData;
|
||||
}
|
||||
return sortedData;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/user_usage_stats/user_activity?days=9999`;
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': apiKey,
|
||||
},
|
||||
})
|
||||
.then(data => {
|
||||
console.log('data');
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/user_usage_stats/user_activity?days=9999`;
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": apiKey,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
|
||||
|
||||
|
||||
// const options = {
|
||||
// day: 'numeric',
|
||||
// month: 'numeric',
|
||||
// year: 'numeric',
|
||||
// hour: 'numeric',
|
||||
// minute: 'numeric',
|
||||
// second: 'numeric',
|
||||
// hour12: true
|
||||
// };
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
return (
|
||||
<div className='Users'>
|
||||
<h1>Users</h1>
|
||||
<table className='user-activity-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
{/* <th onClick={() => handleSort('user_id')}>User ID</th> */}
|
||||
<th></th>
|
||||
<th onClick={() => handleSort('user_name')}>User</th>
|
||||
<th onClick={() => handleSort('item_name')}>Last Watched</th>
|
||||
<th onClick={() => handleSort('client_name')}>Last Client</th>
|
||||
<th onClick={() => handleSort('total_count')}>Total Plays</th>
|
||||
<th onClick={() => handleSort('total_play_time')}>Total Watch Time</th>
|
||||
<th onClick={() => handleSort('last_seen')}>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
sortedData.map(item => (
|
||||
<tr key={item.user_id}>
|
||||
<td>{(item.has_image) ? <img className='card-user-image' src={config.hostUrl + '/Users/' + item.user_id + '/Images/Primary?quality=10'} alt='' /> : <AccountCircleFillIcon color="#fff" size={30}/> }</td>
|
||||
<td>{item.user_name}</td>
|
||||
<td>{item.item_name}</td>
|
||||
<td>{item.client_name}</td>
|
||||
<td>{item.total_count}</td>
|
||||
<td>{item.total_play_time}</td>
|
||||
<td>{item.last_seen} ago</td>
|
||||
{/* <td>{new Date(item.latest_date).toLocaleString('en-GB', options).replace(',', '')}</td> */}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (data.length === 0) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Loading />;
|
||||
}
|
||||
const sortedData = sortData(data, sortConfig);
|
||||
return (
|
||||
<div className="Users">
|
||||
<h1>Users</h1>
|
||||
<table className="user-activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th onClick={() => handleSort("user_name")}>User</th>
|
||||
<th onClick={() => handleSort("item_name")}>Last Watched</th>
|
||||
<th onClick={() => handleSort("client_name")}>Last Client</th>
|
||||
<th onClick={() => handleSort("total_count")}>Total Plays</th>
|
||||
<th onClick={() => handleSort("total_play_time")}>
|
||||
Total Watch Time
|
||||
</th>
|
||||
<th onClick={() => handleSort("last_seen")}>Last Seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedData.map((item) => (
|
||||
<tr key={item.user_id}>
|
||||
<td>
|
||||
{item.has_image ? (
|
||||
<img
|
||||
className="card-user-image"
|
||||
src={
|
||||
config.hostUrl +
|
||||
"/Users/" +
|
||||
item.user_id +
|
||||
"/Images/Primary?quality=10"
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
<AccountCircleFillIcon color="#fff" size={30} />
|
||||
)}
|
||||
</td>
|
||||
<td>{item.user_name}</td>
|
||||
<td>{item.item_name}</td>
|
||||
<td>{item.client_name}</td>
|
||||
<td>{item.total_count}</td>
|
||||
<td>{item.total_play_time}</td>
|
||||
<td>{item.last_seen} ago</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserActivity;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user