mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Add recently played(WIP). centralized API function
This commit is contained in:
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -9,7 +9,7 @@
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://10.0.0.99:3000",
|
||||
"url": "http://10.0.0.20:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,17 +10,35 @@ router.get('/test', async (req, res) => {
|
||||
});
|
||||
|
||||
router.get('/getconfig', async (req, res) => {
|
||||
const { rows } = await db.query('SELECT * FROM app_config');
|
||||
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) => {
|
||||
const { JF_HOST, JF_API_KEY } = req.body;
|
||||
|
||||
console.log(req.body);
|
||||
const { rows } = await db.query('UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2', [JF_HOST, JF_API_KEY]);
|
||||
console.log(`ENDPOINT CALLED: /setconfig: `+rows);
|
||||
res.send(rows);
|
||||
const { rows } = await db.query('UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1', [JF_HOST, JF_API_KEY]);
|
||||
res.send(rows);
|
||||
// const { existing } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
// console.log("Lenght: "+existing.rows[0].length );
|
||||
// if(existing != undefined && existing.length != 0)
|
||||
// {
|
||||
// console.log("Update Config");
|
||||
// const { rows } = await db.query('UPDATE app_config SET "JF_HOST"=$1, "JF_API_KEY"=$2 where "ID"=1', [JF_HOST, JF_API_KEY]);
|
||||
// res.send(rows);
|
||||
// }
|
||||
// else{
|
||||
// console.log("Insert Config");
|
||||
// const { rows } = await db.query('INSERT into app_config VALUES ( $1, $2, null, null)', [JF_HOST, JF_API_KEY]);
|
||||
// res.send(rows);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
console.log(`ENDPOINT CALLED: /setconfig: `);
|
||||
|
||||
});
|
||||
module.exports = router;
|
||||
|
||||
45
src/App.js
45
src/App.js
@@ -1,10 +1,17 @@
|
||||
// import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Routes,
|
||||
Route,
|
||||
} from "react-router-dom";
|
||||
|
||||
import Config from './lib/config';
|
||||
|
||||
import Loading from './pages/components/loading';
|
||||
|
||||
import Setup from './pages/setup';
|
||||
|
||||
|
||||
import SideNav from './pages/components/sidenav';
|
||||
import Home from './pages/home';
|
||||
@@ -13,9 +20,46 @@ import Activity from './pages/activity';
|
||||
import UserActivity from './pages/useractivity';
|
||||
import Libraries from './pages/libraries';
|
||||
|
||||
import RecentlyPlayed from './pages/components/recentlyplayed';
|
||||
|
||||
import UserData from './pages/userdata';
|
||||
|
||||
function App() {
|
||||
|
||||
const [config, setConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
}, [config]);
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!config || config.apiKey ==null) {
|
||||
return <Setup />;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<SideNav />
|
||||
@@ -28,6 +72,7 @@ function App() {
|
||||
<Route path="/libraries" element={<Libraries />} />
|
||||
<Route path="/usersactivity" element={<UserActivity />} />
|
||||
<Route path="/userdata" element={<UserData />} />
|
||||
<Route path="/recent" element={<RecentlyPlayed />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
126
src/classes/jellyfin-api.js
Normal file
126
src/classes/jellyfin-api.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Component } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
|
||||
class API extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async getSessions() {
|
||||
try {
|
||||
const config = await Config();
|
||||
const url = `${config.hostUrl}/Sessions`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getActivityData(limit) {
|
||||
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,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getAdminUser() {
|
||||
try {
|
||||
const config = await Config();
|
||||
const url = `${config.hostUrl}/Users`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
},
|
||||
});
|
||||
const adminUser = response.data.filter(user => user.Policy.IsAdministrator === true);
|
||||
return adminUser || null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getLibraries() {
|
||||
|
||||
try {
|
||||
const config = await Config();
|
||||
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,
|
||||
},
|
||||
});
|
||||
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 url = `${config.hostUrl}/users/${userid}/Items?ParentID=${itemID}`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
},
|
||||
});
|
||||
return response.data.Items;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
return response.data.Items;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default API;
|
||||
@@ -2,7 +2,7 @@ import { Component } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
|
||||
class GetSeries extends Component {
|
||||
class sync extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@@ -11,10 +11,50 @@ class GetSeries extends Component {
|
||||
}
|
||||
|
||||
|
||||
async getData() {
|
||||
async getAdminUser() {
|
||||
try {
|
||||
const config = await Config();
|
||||
const url = `${config.hostUrl}/users/5f63950a2339462196eb8cead70cae7e/Items?ParentID=9d7ad6afe9afa2dab1a2f6e00ad28fa6`;
|
||||
const url = `${config.hostUrl}/Users`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
},
|
||||
});
|
||||
const adminUser = response.data.filter(user => user.Policy.IsAdministrator === true);
|
||||
return adminUser || null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getLibraries() {
|
||||
|
||||
try {
|
||||
const config = await Config();
|
||||
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,
|
||||
},
|
||||
});
|
||||
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 url = `${config.hostUrl}/users/${userid}/Items?ParentID=${itemID}`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
@@ -26,6 +66,8 @@ class GetSeries extends Component {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default GetSeries;
|
||||
export default sync;
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios';
|
||||
|
||||
async function Config() {
|
||||
try {
|
||||
const response = await axios.get('http://10.0.0.99:3003/api/getconfig');
|
||||
const response = await axios.get('http://10.0.0.20:3003/api/getconfig');
|
||||
const { JF_HOST, JF_API_KEY, APP_USER, APP_PASSWORD } = response.data[0];
|
||||
return { hostUrl: JF_HOST, apiKey: JF_API_KEY, username: APP_USER, password: APP_PASSWORD };
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,6 +5,9 @@ import FileListFillIcon from 'remixicon-react/FileListFillIcon';
|
||||
import BarChartFillIcon from 'remixicon-react/BarChartFillIcon';
|
||||
import SettingsFillIcon from 'remixicon-react/SettingsFillIcon';
|
||||
import GalleryFillIcon from 'remixicon-react/GalleryFillIcon';
|
||||
import UserFillIcon from 'remixicon-react/UserFillIcon';
|
||||
|
||||
import ReactjsFillIcon from 'remixicon-react/ReactjsFillIcon';
|
||||
|
||||
export const navData = [
|
||||
{
|
||||
@@ -24,23 +27,30 @@ export const navData = [
|
||||
icon: <GalleryFillIcon />,
|
||||
text: "Libraries",
|
||||
link: "libraries"
|
||||
},
|
||||
} ,
|
||||
{
|
||||
id: 3,
|
||||
icon: <UserFillIcon />,
|
||||
text: "Recently Played",
|
||||
link: "recent"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: <BarChartFillIcon />,
|
||||
text: "User Activity",
|
||||
link: "usersactivity"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: <BarChartFillIcon />,
|
||||
text: "User Data",
|
||||
id: 5,
|
||||
icon: <ReactjsFillIcon />,
|
||||
text: "Library Data",
|
||||
link: "userdata"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
id: 6,
|
||||
icon: <SettingsFillIcon />,
|
||||
text: "Settings",
|
||||
link: "settings"
|
||||
}
|
||||
|
||||
]
|
||||
7
src/models/libraryItem.js
Normal file
7
src/models/libraryItem.js
Normal file
@@ -0,0 +1,7 @@
|
||||
class libraryItem {
|
||||
constructor(id, name, email) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
import API from '../classes/jellyfin-api';
|
||||
|
||||
import '../App.css'
|
||||
|
||||
@@ -10,55 +9,30 @@ import Loading from './components/loading';
|
||||
|
||||
function Activity() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
let _api= new API()
|
||||
|
||||
const fetchData = () => {
|
||||
if (config) {
|
||||
const url = `${config.hostUrl}/System/ActivityLog/Entries?limit=30`;
|
||||
const apiKey = config.apiKey;
|
||||
|
||||
axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': apiKey,
|
||||
},
|
||||
})
|
||||
.then(newData => {
|
||||
if (data && data.length > 0) {
|
||||
const newDataOnly = newData.data.Items.filter(item => {
|
||||
return !data.some(existingItem => existingItem.Id === item.Id);
|
||||
});
|
||||
setData([...newDataOnly, ...data.slice(0, data.length - newDataOnly.length)]);
|
||||
} else {
|
||||
setData(newData.data.Items);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
_api.getActivityData(30).then((ActivityData) => {
|
||||
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(ActivityData.Items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 2000);
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data,config]);
|
||||
}, [data]);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -70,7 +44,7 @@ function Activity() {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: true
|
||||
hour12: false
|
||||
};
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
|
||||
69
src/pages/components/recent-card.js
Normal file
69
src/pages/components/recent-card.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function RecentCard(props) {
|
||||
|
||||
|
||||
return (
|
||||
<div key={props.data.recent.Id} className='recent-card' >
|
||||
|
||||
|
||||
<div className='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-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;
|
||||
68
src/pages/components/recentlyplayed.js
Normal file
68
src/pages/components/recentlyplayed.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
// import axios from 'axios';
|
||||
import Config from '../../lib/config';
|
||||
import API from '../../classes/jellyfin-api';
|
||||
|
||||
|
||||
import "../css/recent.css"
|
||||
// import "../../App.css"
|
||||
|
||||
|
||||
import RecentCard from './recent-card';
|
||||
|
||||
|
||||
import Loading from './loading';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function RecentlyPlayed() {
|
||||
const [data, setData] = 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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
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'>
|
||||
{data &&
|
||||
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,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
// import axios from 'axios';
|
||||
import Config from '../../lib/config';
|
||||
import API from '../../classes/jellyfin-api';
|
||||
|
||||
import "../css/sessions.css"
|
||||
// import "../../App.css"
|
||||
@@ -22,45 +23,23 @@ function Sessions() {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const _api = new API();
|
||||
const fetchData = () => {
|
||||
_api.getSessions().then((SessionData) => {
|
||||
setData(SessionData);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Config().then(config => {
|
||||
console.log('hit api');
|
||||
// let error_counter=0;
|
||||
setURL(config.hostUrl);
|
||||
const url = `${config.hostUrl}/sessions`;
|
||||
const fetchData = () => {
|
||||
axios.get(url, {
|
||||
headers: {
|
||||
'X-MediaBrowser-Token': config.apiKey,
|
||||
},
|
||||
})
|
||||
.then(response => setData(response.data))
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
// error_counter++;
|
||||
// console.log(error_counter);
|
||||
// if(error_counter>9)
|
||||
// {
|
||||
// console.log('Terminating');
|
||||
// return () => clearInterval(intervalId);
|
||||
// }
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// fetchData();
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}).catch(error => {
|
||||
// if (error.code === 'ERR_NETWORK') {
|
||||
// console.log(errorHandler.error_count);
|
||||
// let _error_count = 1;
|
||||
// let _error_message = '';
|
||||
// seterrorHandler({ error_count: _error_count, error_message: _error_message });
|
||||
// }
|
||||
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
|
||||
const intervalId = setInterval(fetchData, 1000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -76,8 +55,8 @@ function 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 }}/>
|
||||
|
||||
<SessionCard data={{ session: session, base_url: base_url }} />
|
||||
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
75
src/pages/css/recent.css
Normal file
75
src/pages/css/recent.css
Normal file
@@ -0,0 +1,75 @@
|
||||
.recent {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(185px, 200px));
|
||||
grid-auto-rows: 340px;/* max-width+offset so 215 + 20*/
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.recent-card
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* background-color: grey; */
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.05);
|
||||
|
||||
height: 320px;
|
||||
width: 185px;
|
||||
border-radius: 4px;
|
||||
|
||||
|
||||
/* Add a third row that takes up remaining space */
|
||||
}
|
||||
|
||||
|
||||
.card-banner {
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
|
||||
}
|
||||
|
||||
.recent-card-details {
|
||||
|
||||
|
||||
width: 100%;
|
||||
height: 30%;
|
||||
position: relative;
|
||||
/* margin: 4px; */
|
||||
|
||||
/* background-color: #f71b1b; */
|
||||
}
|
||||
|
||||
.recent-card-item-name {
|
||||
width: 185px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
|
||||
|
||||
/*
|
||||
|
||||
position: absolute; */
|
||||
}
|
||||
|
||||
.recent-card-last-played{
|
||||
width: 185px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
color: white;
|
||||
|
||||
background-color: grey;
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.05);
|
||||
|
||||
max-height: 215px;
|
||||
max-width: 500px;
|
||||
|
||||
117
src/pages/css/setup.css
Normal file
117
src/pages/css/setup.css
Normal file
@@ -0,0 +1,117 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@500&display=swap');
|
||||
/* *{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'poppins',sans-serif;
|
||||
} */
|
||||
|
||||
|
||||
section{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
/* background: url('background6.jpg')no-repeat; */
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
.form-box{
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 450px;
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255,255,255,0.5);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(15px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
h2{
|
||||
font-size: 2em;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.inputbox{
|
||||
position: relative;
|
||||
margin: 30px 0;
|
||||
width: 310px;
|
||||
border-bottom: 2px solid #fff;
|
||||
}
|
||||
.inputbox label{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 5px;
|
||||
transform: translateY(-50%);
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
pointer-events: none;
|
||||
transition: .2s;
|
||||
}
|
||||
input:focus ~ label,
|
||||
input:valid ~ label{
|
||||
top: -15px;
|
||||
}
|
||||
.inputbox input {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
}
|
||||
.inputbox ion-icon{
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
color: #fff;
|
||||
font-size: 1.2em;
|
||||
top: 20px;
|
||||
}
|
||||
.forget{
|
||||
margin: -15px 0 15px ;
|
||||
font-size: .9em;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.forget label input{
|
||||
margin-right: 3px;
|
||||
|
||||
}
|
||||
.forget label a{
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.forget label a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
.setup-button{
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-radius: 40px;
|
||||
background: #fff;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.register{
|
||||
font-size: .9em;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin: 25px 0 10px;
|
||||
}
|
||||
.register p a{
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.register p a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
160
src/pages/setup.js
Normal file
160
src/pages/setup.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import Config from '../lib/config';
|
||||
|
||||
import './css/setup.css'
|
||||
|
||||
// import Loading from './components/loading';
|
||||
|
||||
function Setup() {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [formValues, setFormValues] = useState({});
|
||||
const [processing, setProcessing] = useState(false);
|
||||
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
|
||||
|
||||
if (response.status === 200) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
// console.log(error.code);
|
||||
if (error.code === 'ERR_NETWORK') {
|
||||
isValid = false;
|
||||
errorMessage = `Unable to connect to Jellyfin Server`;
|
||||
} else
|
||||
if (error.response.status === 401) {
|
||||
isValid = false;
|
||||
errorMessage = `Error ${error.response.status} 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 });
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
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');
|
||||
setProcessing(false);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
|
||||
return;
|
||||
})
|
||||
.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') {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<button type="submit" className='setup-button'>{processing ? 'Validating...' : submitButtonText}</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Setup;
|
||||
@@ -1,16 +1,22 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import GetSeries from '../classes/sync';
|
||||
// import sync from '../classes/sync';
|
||||
import './css/libraries.css';
|
||||
import Loading from './components/loading';
|
||||
|
||||
import API from '../classes/jellyfin-api';
|
||||
|
||||
function UserData() {
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const seriesInstance = new GetSeries(); // create an instance of the GetSeries class
|
||||
seriesInstance.getData().then((seriesData) => {
|
||||
const seriesInstance = new API(); // create an instance of the GetSeries class
|
||||
seriesInstance.getLibraries().then((seriesData) => {
|
||||
setData(seriesData);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}, []); // run this effect only once, when the component mounts
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user