mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-19 00:37:22 +01:00
Item Cards and stats
Added drill down views for individual items Created view to see basic metadata for libraries Created Table to store item metadata such as file size and codec support Scrape and display File and library sizes Removed a redundant view(TBH there's still alot left) Amended some UI elements Started work on backup code WIP (Does not work) Changed out Nivo to recharts for stat graphs to remove <0 behaviour of charts
This commit is contained in:
@@ -86,6 +86,102 @@ router.post("/getLibraryItems", async (req, res) => {
|
||||
console.log(`ENDPOINT CALLED: /getLibraryItems: `);
|
||||
});
|
||||
|
||||
router.post("/getSeasons", async (req, res) => {
|
||||
try{
|
||||
const { Id } = req.body;
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT * FROM jf_library_seasons where "SeriesId"='${Id}'`
|
||||
);
|
||||
console.log({ Id: Id });
|
||||
res.send(rows);
|
||||
|
||||
|
||||
}catch(error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getSeasons: `);
|
||||
});
|
||||
|
||||
router.post("/getEpisodes", async (req, res) => {
|
||||
try{
|
||||
const { Id } = req.body;
|
||||
const { rows } = await db.query(
|
||||
`SELECT * FROM jf_library_episodes where "SeasonId"='${Id}'`
|
||||
);
|
||||
console.log({ Id: Id });
|
||||
res.send(rows);
|
||||
|
||||
|
||||
}catch(error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getEpisodes: `);
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.post("/getItemDetails", async (req, res) => {
|
||||
try{
|
||||
|
||||
const { Id } = req.body;
|
||||
|
||||
|
||||
//
|
||||
|
||||
let query= `SELECT im."Name" "FileName",im.*,i.* FROM jf_library_items i left join jf_item_info im on i."Id" = im."Id" where i."Id"='${Id}'`;
|
||||
|
||||
|
||||
|
||||
const { rows:items } = await db.query(
|
||||
query
|
||||
);
|
||||
|
||||
if(items.length===0)
|
||||
{
|
||||
query=`SELECT im."Name" "FileName",im.*,s.* FROM jf_library_seasons s left join jf_item_info im on s."Id" = im."Id" where s."Id"='${Id}'`;
|
||||
const { rows:seasons } = await db.query(
|
||||
query
|
||||
);
|
||||
|
||||
if(seasons.length===0)
|
||||
{
|
||||
query=`SELECT im."Name" "FileName",im.*,e.* FROM jf_library_episodes e join jf_item_info im on e."EpisodeId" = im."Id" where e."EpisodeId"='${Id}'`;
|
||||
const { rows:episodes } = await db.query(
|
||||
query
|
||||
);
|
||||
|
||||
res.send(episodes);
|
||||
|
||||
}else{
|
||||
|
||||
res.send(seasons);
|
||||
|
||||
}
|
||||
|
||||
}else{
|
||||
|
||||
res.send(items);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}catch(error)
|
||||
{
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
console.log(`ENDPOINT CALLED: /getLibraryItems: `);
|
||||
});
|
||||
|
||||
|
||||
router.get("/getHistory", async (req, res) => {
|
||||
try{
|
||||
|
||||
|
||||
158
backend/backup.js
Normal file
158
backend/backup.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const { Router } = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Database connection parameters
|
||||
const postgresUser = process.env.POSTGRES_USER;
|
||||
const postgresPassword = process.env.POSTGRES_PASSWORD;
|
||||
const postgresIp = process.env.POSTGRES_IP;
|
||||
const postgresPort = process.env.POSTGRES_PORT;
|
||||
const postgresDatabase = process.env.POSTGRES_DATABASE || 'jfstat';
|
||||
|
||||
// Tables to back up
|
||||
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity'];
|
||||
|
||||
// Backup function
|
||||
async function backup() {
|
||||
const pool = new Pool({
|
||||
user: postgresUser,
|
||||
password: postgresPassword,
|
||||
host: postgresIp,
|
||||
port: postgresPort,
|
||||
database: postgresDatabase
|
||||
});
|
||||
|
||||
// Get data from each table and append it to the backup file
|
||||
const backupPath = './backup-data/backup.json';
|
||||
const stream = fs.createWriteStream(backupPath, { flags: 'a' });
|
||||
const backup_data=[];
|
||||
for (let table of tables) {
|
||||
const query = `SELECT * FROM ${table}`;
|
||||
|
||||
const { rows } = await pool.query(query);
|
||||
console.log(`Reading ${rows.length} rows for table ${table}`);
|
||||
|
||||
backup_data.push({[table]:rows});
|
||||
// stream.write(JSON.stringify(backup_data));
|
||||
|
||||
}
|
||||
stream.write(JSON.stringify(backup_data));
|
||||
stream.end();
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
// Restore function
|
||||
async function restore() {
|
||||
// const pool = new Pool({
|
||||
// user: 'postgres',
|
||||
// password: 'mypassword',
|
||||
// host: postgresIp,
|
||||
// port: 25432,
|
||||
// database: postgresDatabase
|
||||
// });
|
||||
|
||||
let user='postgres';
|
||||
let password='mypassword';
|
||||
let host=postgresIp;
|
||||
let port=25432;
|
||||
let database= postgresDatabase;
|
||||
|
||||
const client = new Pool({
|
||||
host,
|
||||
port,
|
||||
database,
|
||||
user,
|
||||
password
|
||||
});
|
||||
const backupPath = './backup-data/backup.json';
|
||||
|
||||
let jsonData;
|
||||
await fs.readFile(backupPath, 'utf8', async (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
jsonData = await JSON.parse(data);
|
||||
});
|
||||
|
||||
console.log(jsonData);
|
||||
if(!jsonData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
jsonData.forEach((library) => {
|
||||
|
||||
console.log(library);
|
||||
});
|
||||
|
||||
// await client.connect();
|
||||
|
||||
|
||||
|
||||
// let tableStarted = false;
|
||||
// let tableColumns = '';
|
||||
// let tableValues = '';
|
||||
// let tableName = '';
|
||||
|
||||
// for await (const line of rl) {
|
||||
// if (!tableStarted && line.startsWith('COPY ')) {
|
||||
// tableName = line.match(/COPY (.*) \(/)[1];
|
||||
// tableStarted = true;
|
||||
// tableColumns = '';
|
||||
// tableValues = '';
|
||||
// } else if (tableStarted && line.startsWith('\.')) {
|
||||
// const insertStatement = `INSERT INTO ${tableName} (${tableColumns}) VALUES ${tableValues};`;
|
||||
// await client.query(insertStatement);
|
||||
// tableStarted = false;
|
||||
// } else if (tableStarted && tableColumns === '') {
|
||||
// tableColumns = line.replace(/\(/g, '').replace(/\)/g, '').replace(/"/g, '').trim();
|
||||
// tableValues = '';
|
||||
// } else if (tableStarted) {
|
||||
// const values = line.replace(/\(/g, '').replace(/\)/g, '').split('\t').map(value => {
|
||||
// if (value === '') {
|
||||
// return null;
|
||||
// }
|
||||
// if (!isNaN(parseFloat(value))) {
|
||||
// return parseFloat(value);
|
||||
// }
|
||||
// return value.replace(/'/g, "''");
|
||||
// });
|
||||
// const rowValues = `(${values.join(',')})`;
|
||||
// tableValues = `${tableValues}${tableValues === '' ? '' : ','}${rowValues}`;
|
||||
// }
|
||||
// }
|
||||
|
||||
// await client.end();
|
||||
}
|
||||
|
||||
// Route handler for backup endpoint
|
||||
router.get('/backup', async (req, res) => {
|
||||
try {
|
||||
await backup();
|
||||
res.send('Backup completed successfully');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send('Backup failed');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/restore', async (req, res) => {
|
||||
try {
|
||||
await restore();
|
||||
res.send('Backup completed successfully');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send('Backup failed');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
29
backend/migrations/024_jf_item_info_table.js
Normal file
29
backend/migrations/024_jf_item_info_table.js
Normal file
@@ -0,0 +1,29 @@
|
||||
exports.up = async function(knex) {
|
||||
try {
|
||||
const tableExists = await knex.schema.hasTable('jf_item_info');
|
||||
if (!tableExists) {
|
||||
await knex.schema.createTable('jf_item_info', function(table) {
|
||||
table.text('Id').notNullable().primary();
|
||||
table.text('Path');
|
||||
table.text('Name');
|
||||
table.bigInteger('Size');
|
||||
table.bigInteger('Bitrate');
|
||||
table.json('MediaStreams');
|
||||
table.text('Type');
|
||||
});
|
||||
|
||||
await knex.raw(`ALTER TABLE IF EXISTS jf_item_info OWNER TO ${process.env.POSTGRES_USER};`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
try {
|
||||
await knex.schema.dropTableIfExists('jf_item_info');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
25
backend/migrations/024_js_library_metadata_view.js
Normal file
25
backend/migrations/024_js_library_metadata_view.js
Normal file
@@ -0,0 +1,25 @@
|
||||
exports.up = async function(knex) {
|
||||
await knex.raw(`
|
||||
CREATE OR REPLACE VIEW public.js_library_metadata AS
|
||||
select
|
||||
l."Id",
|
||||
l."Name",
|
||||
sum(ii."Size") "Size",
|
||||
count(*) files
|
||||
from jf_libraries l
|
||||
join jf_library_items i
|
||||
on i."ParentId"=l."Id"
|
||||
left join jf_library_episodes e
|
||||
on e."SeriesId"=i."Id"
|
||||
left join jf_item_info ii
|
||||
on (ii."Id"=i."Id" or ii."Id"=e."EpisodeId")
|
||||
group by l."Id",l."Name"
|
||||
`).catch(function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.raw(`DROP VIEW js_library_metadata;`);
|
||||
};
|
||||
|
||||
25
backend/models/jf_item_info.js
Normal file
25
backend/models/jf_item_info.js
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
const jf_item_info_columns = [
|
||||
"Id",
|
||||
"Path",
|
||||
"Name",
|
||||
"Size",
|
||||
"Bitrate",
|
||||
"MediaStreams",
|
||||
"Type",
|
||||
];
|
||||
|
||||
const jf_item_info_mapping = (item, typeOverride) => ({
|
||||
Id: item.EpisodeId || item.Id,
|
||||
Path: item.Path,
|
||||
Name: item.Name,
|
||||
Size: item.Size,
|
||||
Bitrate: item.Bitrate,
|
||||
MediaStreams:JSON.stringify(item.MediaStreams),
|
||||
Type: typeOverride !== undefined ? typeOverride : item.Type,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_item_info_columns,
|
||||
jf_item_info_mapping,
|
||||
};
|
||||
@@ -22,7 +22,7 @@
|
||||
];
|
||||
|
||||
const jf_library_episodes_mapping = (item) => ({
|
||||
Id: item.Id + item.ParentId,
|
||||
Id: item.Id + item.SeasonId,
|
||||
EpisodeId: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
@@ -41,7 +41,7 @@
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesId: item.SeriesId,
|
||||
SeasonId: item.ParentId,
|
||||
SeasonId: item.SeasonId,
|
||||
SeasonName: item.SeasonName,
|
||||
SeriesName: item.SeriesName,
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
? item.ParentBackdropImageTags[0]
|
||||
: null,
|
||||
SeriesName: item.SeriesName,
|
||||
SeriesId: item.ParentId,
|
||||
SeriesId: item.SeriesId,
|
||||
SeriesPrimaryImageTag: item.SeriesPrimaryImageTag,
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ const authRouter= require('./auth');
|
||||
const apiRouter = require('./api');
|
||||
const syncRouter = require('./sync');
|
||||
const statsRouter = require('./stats');
|
||||
const backupRouter = require('./backup');
|
||||
const ActivityMonitor = require('./watchdog/ActivityMonitor');
|
||||
|
||||
|
||||
@@ -50,6 +51,7 @@ app.use('/auth', authRouter); // mount the API router at /api, with JWT middlewa
|
||||
app.use('/api', verifyToken, apiRouter); // mount the API router at /api, with JWT middleware
|
||||
app.use('/sync', verifyToken, syncRouter); // mount the API router at /sync, with JWT middleware
|
||||
app.use('/stats', verifyToken, statsRouter); // mount the API router at /stats, with JWT middleware
|
||||
app.use('/data', backupRouter); // mount the API router at /stats, with JWT middleware
|
||||
|
||||
try{
|
||||
createdb.createDatabase().then((result) => {
|
||||
|
||||
@@ -4,11 +4,6 @@ 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) => {
|
||||
try {
|
||||
const { rows } = await db.query("SELECT * FROM jf_library_count_view");
|
||||
@@ -202,7 +197,6 @@ router.post("/getGlobalUserStats", async (req, res) => {
|
||||
if (hours === undefined) {
|
||||
_hours = 24;
|
||||
}
|
||||
console.log(`select * from fs_user_stats(${_hours},'${userid}')`);
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_user_stats(${_hours},'${userid}')`
|
||||
);
|
||||
@@ -246,7 +240,6 @@ router.post("/getGlobalLibraryStats", async (req, res) => {
|
||||
if (hours === undefined) {
|
||||
_hours = 24;
|
||||
}
|
||||
console.log(`select * from fs_library_stats(${_hours},'${libraryid}')`);
|
||||
const { rows } = await db.query(
|
||||
`select * from fs_library_stats(${_hours},'${libraryid}')`
|
||||
);
|
||||
@@ -267,6 +260,16 @@ router.get("/getLibraryCardStats", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/getLibraryMetadata", async (req, res) => {
|
||||
try {
|
||||
const { rows } = await db.query("select * from js_library_metadata");
|
||||
res.send(rows);
|
||||
} catch (error) {
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.post("/getLibraryLastPlayed", async (req, res) => {
|
||||
try {
|
||||
const { libraryid } = req.body;
|
||||
@@ -406,6 +409,28 @@ const finalData = {libraries:libraries,stats:Object.values(reorganizedData)};
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/getGlobalItemStats", async (req, res) => {
|
||||
try {
|
||||
const { hours,itemid } = req.body;
|
||||
let _hours = hours;
|
||||
if (hours === undefined) {
|
||||
_hours = 24;
|
||||
}
|
||||
const { rows } = await db.query(
|
||||
`select count(*)"Plays",
|
||||
sum("PlaybackDuration") total_playback_duration
|
||||
from jf_playback_activity
|
||||
where
|
||||
("EpisodeId"='${itemid}' OR "SeasonId"='${itemid}' OR "NowPlayingItemId"='${itemid}')
|
||||
AND jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - INTERVAL '1 hour' * ${_hours} AND NOW();`
|
||||
);
|
||||
res.send(rows[0]);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
215
backend/sync.js
215
backend/sync.js
@@ -12,6 +12,7 @@ const {jf_libraries_columns,jf_libraries_mapping,} = require("./models/jf_librar
|
||||
const {jf_library_items_columns,jf_library_items_mapping,} = require("./models/jf_library_items");
|
||||
const {jf_library_seasons_columns,jf_library_seasons_mapping,} = require("./models/jf_library_seasons");
|
||||
const {jf_library_episodes_columns,jf_library_episodes_mapping,} = require("./models/jf_library_episodes");
|
||||
const {jf_item_info_columns,jf_item_info_mapping,} = require("./models/jf_item_info");
|
||||
|
||||
const {jf_users_columns,jf_users_mapping,} = require("./models/jf_users");
|
||||
|
||||
@@ -83,26 +84,46 @@ class sync {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async getSeasonsAndEpisodes(showId,userid) {
|
||||
const allSeasons = [];
|
||||
const allEpisodes = [];
|
||||
let seasonItems = await this.getItem(showId,userid);
|
||||
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,userid);
|
||||
const episodeWithParent = episodeItems.map((items) => ({
|
||||
...items,
|
||||
...{ ParentId: season.Id },
|
||||
}));
|
||||
allEpisodes.push(...episodeWithParent);
|
||||
}
|
||||
|
||||
return { allSeasons: allSeasons, allEpisodes: allEpisodes };
|
||||
async getSeasonsAndEpisodes(itemID, type) {
|
||||
try {
|
||||
|
||||
let url = `${this.hostUrl}/shows/${itemID}/${type}`;
|
||||
if (itemID !== undefined) {
|
||||
url += `?ParentID=${itemID}`;
|
||||
}
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": this.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data.Items;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async getItemInfo(itemID,userid) {
|
||||
try {
|
||||
|
||||
let url = `${this.hostUrl}/Items/${itemID}/playbackinfo?userId=${userid}`;
|
||||
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"X-MediaBrowser-Token": this.apiKey,
|
||||
},
|
||||
});
|
||||
|
||||
const results = response.data.MediaSources;
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
////////////////////////////////////////API Methods
|
||||
@@ -222,7 +243,7 @@ async function syncLibraryItems()
|
||||
}
|
||||
|
||||
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/2" });
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/3" });
|
||||
|
||||
sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",});
|
||||
|
||||
@@ -288,14 +309,14 @@ async function syncLibraryItems()
|
||||
sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",});
|
||||
sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
|
||||
|
||||
const { rows: cleanup } = await db.query('DELETE FROM jf_playback_activity where "NowPlayingItemId" not in (select "Id" from jf_library_items)' );
|
||||
sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
|
||||
// const { rows: cleanup } = await db.query('DELETE FROM jf_playback_activity where "NowPlayingItemId" not in (select "Id" from jf_library_items)' );
|
||||
// sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
|
||||
|
||||
}
|
||||
|
||||
async function syncShowItems()
|
||||
{
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/2" });
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/3" });
|
||||
sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",});
|
||||
|
||||
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
|
||||
@@ -313,11 +334,12 @@ async function syncShowItems()
|
||||
let deleteSeasonsCount = 0;
|
||||
let deleteEpisodeCount = 0;
|
||||
|
||||
const admins = await _sync.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
//loop for each show
|
||||
for (const show of shows) {
|
||||
const data = await _sync.getSeasonsAndEpisodes(show.Id,userid);
|
||||
const allSeasons = await _sync.getSeasonsAndEpisodes(show.Id,'Seasons');
|
||||
const allEpisodes =await _sync.getSeasonsAndEpisodes(show.Id,'Episodes');
|
||||
|
||||
|
||||
|
||||
const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
@@ -341,20 +363,20 @@ async function syncShowItems()
|
||||
|
||||
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(jf_library_seasons_mapping);
|
||||
seasonsToInsert = await allSeasons.map(jf_library_seasons_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
seasonsToInsert = await data.allSeasons
|
||||
seasonsToInsert = await allSeasons
|
||||
.filter((row) => !existingIdsSeasons.includes(row.Id))
|
||||
.map(jf_library_seasons_mapping);
|
||||
}
|
||||
|
||||
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(jf_library_episodes_mapping);
|
||||
episodesToInsert = await allEpisodes.map(jf_library_episodes_mapping);
|
||||
} else {
|
||||
// otherwise, filter only new data to insert
|
||||
episodesToInsert = await data.allEpisodes.filter((row) => !existingIdsEpisodes.includes(row.Id + row.ParentId)).map(jf_library_episodes_mapping);
|
||||
episodesToInsert = await allEpisodes.filter((row) => !existingIdsEpisodes.includes(row.Id + row.SeasonId)).map(jf_library_episodes_mapping);
|
||||
}
|
||||
|
||||
///insert delete seasons
|
||||
@@ -370,7 +392,7 @@ async function syncShowItems()
|
||||
});
|
||||
}
|
||||
}
|
||||
const toDeleteIds = existingIdsSeasons.filter((id) =>!data.allSeasons.some((row) => row.Id === id ));
|
||||
const toDeleteIds = existingIdsSeasons.filter((id) =>!allSeasons.some((row) => row.Id === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (toDeleteIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_library_seasons",toDeleteIds);
|
||||
@@ -395,7 +417,7 @@ async function syncShowItems()
|
||||
}
|
||||
}
|
||||
|
||||
const toDeleteEpisodeIds = existingIdsEpisodes.filter((id) =>!data.allEpisodes.some((row) => (row.Id + row.ParentId) === id ));
|
||||
const toDeleteEpisodeIds = existingIdsEpisodes.filter((id) =>!allEpisodes.some((row) => (row.Id + row.ParentId) === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (toDeleteEpisodeIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_library_episodes",toDeleteEpisodeIds);
|
||||
@@ -407,8 +429,6 @@ async function syncShowItems()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
sendMessageToClients({ Message: "Sync complete for " + show.Name });
|
||||
}
|
||||
|
||||
@@ -416,6 +436,122 @@ async function syncShowItems()
|
||||
sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
|
||||
sendMessageToClients({ color: "yellow", Message: "Sync Complete" });
|
||||
}
|
||||
|
||||
async function syncItemInfo()
|
||||
{
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
|
||||
sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
|
||||
|
||||
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: Items } = await db.query(`SELECT * FROM public.jf_library_items where "Type" not in ('Series','Folder')`);
|
||||
const { rows: Episodes } = await db.query(`SELECT * FROM public.jf_library_episodes`);
|
||||
|
||||
let insertItemInfoCount = 0;
|
||||
let insertEpisodeInfoCount = 0;
|
||||
let deleteItemInfoCount = 0;
|
||||
let deleteEpisodeInfoCount = 0;
|
||||
|
||||
const admins = await _sync.getAdminUser();
|
||||
const userid = admins[0].Id;
|
||||
//loop for each Movie
|
||||
for (const Item of Items) {
|
||||
const data = await _sync.getItemInfo(Item.Id,userid);
|
||||
|
||||
const existingItemInfo = await db.query(`SELECT * FROM public.jf_item_info where "Id" = '${Item.Id}'`).then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
let ItemInfoToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingItemInfo.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
ItemInfoToInsert = await data.map(item => jf_item_info_mapping(item, 'Item'));
|
||||
} else {
|
||||
ItemInfoToInsert = await data.filter((row) => !existingItemInfo.includes(row.Id))
|
||||
.map(item => jf_item_info_mapping(item, 'Item'));
|
||||
}
|
||||
|
||||
if (ItemInfoToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_item_info",ItemInfoToInsert,jf_item_info_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertItemInfoCount += ItemInfoToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
const toDeleteItemInfoIds = existingItemInfo.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (toDeleteItemInfoIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_item_info",toDeleteItemInfoIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteItemInfoCount +=toDeleteItemInfoIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//loop for each Episode
|
||||
console.log("Episode")
|
||||
for (const Episode of Episodes) {
|
||||
const data = await _sync.getItemInfo(Episode.EpisodeId,userid);
|
||||
|
||||
|
||||
const existingEpisodeItemInfo = await db.query(`SELECT * FROM public.jf_item_info where "Id" = '${Episode.EpisodeId}'`).then((res) => res.rows.map((row) => row.Id));
|
||||
|
||||
|
||||
let EpisodeInfoToInsert = [];
|
||||
//filter fix if jf_libraries is empty
|
||||
|
||||
if (existingEpisodeItemInfo.length === 0) {
|
||||
// if there are no existing Ids in the table, map all items in the data array to the expected format
|
||||
EpisodeInfoToInsert = await data.map(item => jf_item_info_mapping(item, 'Episode'));
|
||||
} else {
|
||||
EpisodeInfoToInsert = await data.filter((row) => !existingEpisodeItemInfo.includes(row.Id))
|
||||
.map(item => jf_item_info_mapping(item, 'Episode'));
|
||||
}
|
||||
|
||||
if (EpisodeInfoToInsert.length !== 0) {
|
||||
let result = await db.insertBulk("jf_item_info",EpisodeInfoToInsert,jf_item_info_columns);
|
||||
if (result.Result === "SUCCESS") {
|
||||
insertEpisodeInfoCount += EpisodeInfoToInsert.length;
|
||||
} else {
|
||||
sendMessageToClients({
|
||||
color: "red",
|
||||
Message: "Error performing bulk insert:" + result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
const toDeleteEpisodeInfoIds = existingEpisodeItemInfo.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
//Bulk delete from db thats no longer on api
|
||||
if (toDeleteEpisodeInfoIds.length > 0) {
|
||||
let result = await db.deleteBulk("jf_item_info",toDeleteEpisodeInfoIds);
|
||||
if (result.Result === "SUCCESS") {
|
||||
deleteEpisodeInfoCount +=toDeleteEpisodeInfoIds.length;
|
||||
} else {
|
||||
sendMessageToClients({color: "red",Message: result.message,});
|
||||
}
|
||||
|
||||
}
|
||||
console.log(Episode.Name)
|
||||
}
|
||||
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",});
|
||||
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes inserted.",});
|
||||
sendMessageToClients({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Removed.",});
|
||||
sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
|
||||
}
|
||||
|
||||
@@ -427,6 +563,7 @@ router.get("/beingSync", async (req, res) => {
|
||||
await syncLibraryFolders();
|
||||
await syncLibraryItems();
|
||||
await syncShowItems();
|
||||
await syncItemInfo();
|
||||
|
||||
res.send();
|
||||
|
||||
@@ -463,4 +600,14 @@ router.get("/writeSeasonsAndEpisodes", async (req, res) => {
|
||||
|
||||
//////////////////////////////////////
|
||||
|
||||
//////////////////////////////////////////////////////writeMediaInfo
|
||||
router.get("/writeMediaInfo", async (req, res) => {
|
||||
await syncItemInfo();
|
||||
res.send();
|
||||
|
||||
});
|
||||
|
||||
//////////////////////////////////////
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -30,6 +30,15 @@ h1{
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h2{
|
||||
color: white;
|
||||
font-weight: lighter !important;
|
||||
margin: 0 !important;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import Users from './pages/users';
|
||||
import UserInfo from './pages/components/user-info';
|
||||
import Libraries from './pages/libraries';
|
||||
import LibraryInfo from './pages/components/library-info';
|
||||
import ItemInfo from './pages/components/item-info';
|
||||
import ErrorPage from './pages/components/general/error';
|
||||
|
||||
|
||||
@@ -128,6 +129,7 @@ if (config && isConfigured && token!==null){
|
||||
<Route path="/users/:UserId" element={<UserInfo />} />
|
||||
<Route path="/libraries" element={<Libraries />} />
|
||||
<Route path="/libraries/:LibraryId" element={<LibraryInfo />} />
|
||||
<Route path="/item/:Id" element={<ItemInfo />} />
|
||||
<Route path="/statistics" element={<Statistics />} />
|
||||
<Route path="/activity" element={<Activity />} />
|
||||
<Route path="/testing" element={<Testing />} />
|
||||
|
||||
@@ -40,9 +40,7 @@ function Activity() {
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
|
||||
@@ -86,7 +86,7 @@ function ActivityTable(props) {
|
||||
<div className='table-rows' key={item.NowPlayingItemId+item.EpisodeId} onClick={() => handleCollapse(item.NowPlayingItemId+item.EpisodeId)}>
|
||||
<div className='table-rows-content'>
|
||||
<div><Link to={`/users/${item.UserId}`}>{item.UserName}</Link></div>
|
||||
<div>{!item.SeriesName ? item.NowPlayingItemName : item.SeriesName+' - '+ item.NowPlayingItemName}</div>
|
||||
<div><Link to={`/item/${item.EpisodeId || item.NowPlayingItemId}`}>{!item.SeriesName ? item.NowPlayingItemName : item.SeriesName+' - '+ item.NowPlayingItemName}</Link></div>
|
||||
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(item.ActivityDateInserted))}</div>
|
||||
<div>{item.results.length+1}</div>
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@ function ActivityTable(props) {
|
||||
|
||||
<div className='table-rows-content bg-grey sub-row' key={sub_item.EpisodeId+index}>
|
||||
<div><Link to={`/users/${sub_item.UserId}`}>{sub_item.UserName}</Link></div>
|
||||
<div>{!sub_item.SeriesName ? sub_item.NowPlayingItemName : sub_item.SeriesName+' - '+ sub_item.NowPlayingItemName}</div>
|
||||
<div><Link to={`/item/${sub_item.EpisodeId || sub_item.Id}`}>{!sub_item.SeriesName ? sub_item.NowPlayingItemName : sub_item.SeriesName+' - '+ sub_item.NowPlayingItemName}</Link></div>
|
||||
<div>{Intl.DateTimeFormat('en-UK', options).format(new Date(sub_item.ActivityDateInserted))}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, {useState} from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
import "../../css/lastplayed.css";
|
||||
|
||||
function formatTime(time) {
|
||||
|
||||
@@ -32,6 +33,7 @@ function LastWatchedCard(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="last-card">
|
||||
<Link to={`/item/${props.data.Id}`}>
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : <Blurhash hash={props.data.PrimaryImageHash} width={'100%'} height={'100%'}/>}
|
||||
<img
|
||||
@@ -47,6 +49,7 @@ function LastWatchedCard(props) {
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="last-item-details">
|
||||
<div className="last-last-played">
|
||||
96
src/pages/components/item-info.js
Normal file
96
src/pages/components/item-info.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import GlobalStats from './item-info/globalStats';
|
||||
import ItemDetails from './item-info/item-details';
|
||||
import MoreItems from "./item-info/more-items";
|
||||
|
||||
import Config from "../../lib/config";
|
||||
import Loading from "./general/loading";
|
||||
|
||||
|
||||
|
||||
function ItemInfo() {
|
||||
const { Id } = useParams();
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if(config){
|
||||
setRefresh(true);
|
||||
try {
|
||||
console.log(Id);
|
||||
const itemData = await axios.post(`/api/getItemDetails`, {
|
||||
Id: Id
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
setData(itemData.data[0]);
|
||||
setRefresh(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
fetchData();
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [config, Id]);
|
||||
|
||||
console.log(data);
|
||||
|
||||
|
||||
if(!data)
|
||||
{
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
if(refresh)
|
||||
{
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ItemDetails data={data} hostUrl={config.hostUrl}/>
|
||||
<GlobalStats ItemId={Id}/>
|
||||
{["Series","Season"].includes(data && data.Type)?
|
||||
<MoreItems data={data}/>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ItemInfo;
|
||||
85
src/pages/components/item-info/globalStats.js
Normal file
85
src/pages/components/item-info/globalStats.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import "../../css/globalstats.css";
|
||||
|
||||
import WatchTimeStats from "./globalstats/watchtimestats";
|
||||
|
||||
function GlobalStats(props) {
|
||||
const [dayStats, setDayStats] = useState({});
|
||||
const [weekStats, setWeekStats] = useState({});
|
||||
const [monthStats, setMonthStats] = useState({});
|
||||
const [allStats, setAllStats] = useState({});
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const dayData = await axios.post(`/stats/getGlobalItemStats`, {
|
||||
hours: (24*1),
|
||||
itemid: props.ItemId,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
setDayStats(dayData.data);
|
||||
|
||||
const weekData = await axios.post(`/stats/getGlobalItemStats`, {
|
||||
hours: (24*7),
|
||||
itemid: props.ItemId,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
setWeekStats(weekData.data);
|
||||
|
||||
const monthData = await axios.post(`/stats/getGlobalItemStats`, {
|
||||
hours: (24*30),
|
||||
itemid: props.ItemId,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
setMonthStats(monthData.data);
|
||||
|
||||
const allData = await axios.post(`/stats/getGlobalItemStats`, {
|
||||
hours: (24*999),
|
||||
itemid: props.ItemId,
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
setAllStats(allData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [props.ItemId,token]);
|
||||
|
||||
// console.log(dayStats);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="py-3">Item Stats</h1>
|
||||
<div className="global-stats-container">
|
||||
<WatchTimeStats data={dayStats} heading={"Last 24 Hours"} />
|
||||
<WatchTimeStats data={weekStats} heading={"Last 7 Days"} />
|
||||
<WatchTimeStats data={monthStats} heading={"Last 30 Days"} />
|
||||
<WatchTimeStats data={allStats} heading={"All Time"} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalStats;
|
||||
65
src/pages/components/item-info/globalstats/watchtimestats.js
Normal file
65
src/pages/components/item-info/globalstats/watchtimestats.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
|
||||
import "../../../css/globalstats.css";
|
||||
|
||||
function WatchTimeStats(props) {
|
||||
|
||||
function formatTime(totalSeconds, numberClassName, labelClassName) {
|
||||
const units = [
|
||||
{ label: 'Day', seconds: 86400 },
|
||||
{ label: 'Hour', seconds: 3600 },
|
||||
{ label: 'Minute', seconds: 60 },
|
||||
];
|
||||
|
||||
const parts = units.reduce((result, { label, seconds }) => {
|
||||
const value = Math.floor(totalSeconds / seconds);
|
||||
if (value) {
|
||||
const formattedValue = <p className={numberClassName}>{value}</p>;
|
||||
const formattedLabel = (
|
||||
<span className={labelClassName}>
|
||||
{label}
|
||||
{value === 1 ? '' : 's'}
|
||||
</span>
|
||||
);
|
||||
result.push(
|
||||
<span key={label} className="time-part">
|
||||
{formattedValue} {formattedLabel}
|
||||
</span>
|
||||
);
|
||||
totalSeconds -= value * seconds;
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<p className={numberClassName}>0</p>{' '}
|
||||
<p className={labelClassName}>Minutes</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="global-stats">
|
||||
<div className="stats-header">
|
||||
<div>{props.heading}</div>
|
||||
</div>
|
||||
|
||||
<div className="play-duration-stats" key={props.data.ItemId}>
|
||||
<p className="stat-value"> {props.data.Plays || 0}</p>
|
||||
<p className="stat-unit" >Plays /</p>
|
||||
|
||||
<>{formatTime(props.data.total_playback_duration || 0,'stat-value','stat-unit')}</>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WatchTimeStats;
|
||||
99
src/pages/components/item-info/item-details.js
Normal file
99
src/pages/components/item-info/item-details.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { Row, Col } from "react-bootstrap";
|
||||
|
||||
|
||||
import "../../css/items/item-details.css";
|
||||
|
||||
|
||||
|
||||
function ItemDetails(props) {
|
||||
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
|
||||
function formatFileSize(sizeInBytes) {
|
||||
const sizeInMB = sizeInBytes / 1048576; // 1 MB = 1048576 bytes
|
||||
if (sizeInMB < 1000) {
|
||||
return `${sizeInMB.toFixed(2)} MB`;
|
||||
} else {
|
||||
const sizeInGB = sizeInMB / 1024; // 1 GB = 1024 MB
|
||||
return `${sizeInGB.toFixed(2)} GB`;
|
||||
}
|
||||
}
|
||||
|
||||
function ticksToTimeString(ticks) {
|
||||
// Convert ticks to seconds
|
||||
const seconds = Math.floor(ticks / 10000000);
|
||||
|
||||
// Calculate hours, minutes, and remaining seconds
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
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")}`;
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="item-detail-container">
|
||||
<Row className="justify-content-center justify-content-md-start">
|
||||
<Col className="col-auto my-4 my-md-0">
|
||||
{props.data.PrimaryImageHash && !loaded ? <Blurhash hash={props.data.PrimaryImageHash} width={'200px'} height={'300px'}/> : null}
|
||||
<img
|
||||
className="item-image"
|
||||
src={
|
||||
props.hostUrl +
|
||||
"/Items/" +
|
||||
(props.data.Type==="Episode"? props.data.SeriesId : props.data.Id) +
|
||||
"/Images/Primary?fillWidth=200&quality=90"
|
||||
}
|
||||
alt=""
|
||||
style={{
|
||||
display: loaded ? "block" :"none"
|
||||
}}
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col >
|
||||
<div className="item-details">
|
||||
<h1 className="">
|
||||
{props.data.SeriesId?
|
||||
<Link to={`/item/${props.data.SeriesId}`}>{props.data.SeriesName || props.data.Name}</Link>
|
||||
:
|
||||
props.data.SeriesName || props.data.Name
|
||||
}
|
||||
|
||||
</h1>
|
||||
|
||||
<div className="my-3">
|
||||
{props.data.CommunityRating ? <p style={{color:"lightgrey", fontSize:"0.8em", fontStyle:"italic"}}>Community Rating: {props.data.CommunityRating}</p> :<></>}
|
||||
{props.data.Type==="Episode"? <p><Link to={`/item/${props.data.SeasonId}`} className="fw-bold">{props.data.SeasonName}</Link> Episode {props.data.IndexNumber} - {props.data.Name}</p> : <></> }
|
||||
{props.data.Type==="Season"? <p>{props.data.Name}</p> : <></> }
|
||||
{props.data.FileName ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">File Name: {props.data.FileName}</p> :<></>}
|
||||
{props.data.Path ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">File Path: {props.data.Path}</p> :<></>}
|
||||
{props.data.RunTimeTicks ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">{props.data.Type==="Series"?"Average Runtime" : "Runtime"}: {ticksToTimeString(props.data.RunTimeTicks)}</p> :<></>}
|
||||
{props.data.Size ? <p style={{color:"lightgrey"}} className="fst-italic fs-6">File Size: {formatFileSize(props.data.Size)}</p> :<></>}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItemDetails;
|
||||
82
src/pages/components/item-info/more-items.js
Normal file
82
src/pages/components/item-info/more-items.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import MoreItemCards from "./more-items/more-items-card";
|
||||
|
||||
import Config from "../../../lib/config";
|
||||
import "../../css/users/user-details.css";
|
||||
|
||||
function MoreItems(props) {
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchData = async () => {
|
||||
if(config)
|
||||
{
|
||||
try {
|
||||
let url=`/api/getSeasons`;
|
||||
if(props.data.Type==='Season')
|
||||
{
|
||||
url=`/api/getEpisodes`;
|
||||
}
|
||||
|
||||
const itemData = await axios.post(url, {
|
||||
Id: props.data.EpisodeId||props.data.Id
|
||||
},{
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
setData(itemData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [config, props]);
|
||||
|
||||
|
||||
if (!data || data.lenght===0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="last-played">
|
||||
<h1 className="my-3">{props.data.Type==="Season" ? "Episodes" : "Seasons"}</h1>
|
||||
<div className="last-played-container">
|
||||
|
||||
{data.map((item) => (
|
||||
<MoreItemCards data={item} base_url={config.hostUrl} key={item.Id}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreItems;
|
||||
67
src/pages/components/item-info/more-items/more-items-card.js
Normal file
67
src/pages/components/item-info/more-items/more-items-card.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, {useState} from "react";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { Link } from "react-router-dom";
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
|
||||
|
||||
|
||||
function MoreItemCards(props) {
|
||||
const { Id } = useParams();
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [fallback, setFallback] = useState(false);
|
||||
return (
|
||||
<div className={props.data.Type==="Episode" ? "last-card episode" : "last-card"}>
|
||||
<Link to={`/item/${ (props.data.Type==="Episode" ? props.data.EpisodeId : props.data.Id) }`}>
|
||||
<div className={props.data.Type==="Episode" ? "last-card-banner episode" : "last-card-banner"}>
|
||||
{props.data.ImageBlurHashes && !loaded ? <Blurhash hash={props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]} width={'100%'} height={'100%'}/> : null}
|
||||
|
||||
{fallback ?
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
:
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
(props.data.Type==="Episode" ? props.data.EpisodeId : props.data.Id) +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
alt=""
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => setFallback(true)}
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="last-item-details">
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
{props.data.Type==="Episode"?
|
||||
<div className="last-item-name"> S{props.data.ParentIndexNumber || 0} - E{props.data.IndexNumber || 0}</div>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreItemCards;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {useState} from "react";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
|
||||
@@ -9,6 +10,7 @@ function RecentlyAddedCard(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="last-card">
|
||||
<Link to={`/item/${props.data.Id}`}>
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : <Blurhash hash={props.data.ImageBlurHashes.Primary[props.data.ImageTags.Primary]} width={'100%'} height={'100%'}/>}
|
||||
<img
|
||||
@@ -24,6 +26,7 @@ function RecentlyAddedCard(props) {
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="last-item-details">
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import ItemCardInfo from "./LastWatched/last-watched-card";
|
||||
// import ItemCardInfo from "./LastWatched/last-watched-card";
|
||||
|
||||
import LastWatchedCard from "../general/last-watched-card";
|
||||
|
||||
|
||||
import Config from "../../../lib/config";
|
||||
import "../../css/users/user-details.css";
|
||||
@@ -61,7 +64,7 @@ function LibraryLastWatched(props) {
|
||||
<h1 className="my-3">Last Watched</h1>
|
||||
<div className="last-played-container">
|
||||
{data.map((item) => (
|
||||
<ItemCardInfo data={item} base_url={config.hostUrl} key={item.Id+item.SeasonNumber+item.EpisodeNumber}/>
|
||||
<LastWatchedCard data={item} base_url={config.hostUrl} key={item.Id+item.SeasonNumber+item.EpisodeNumber}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,33 @@ import Col from 'react-bootstrap/Col';
|
||||
|
||||
function LibraryCard(props) {
|
||||
|
||||
function formatFileSize(sizeInBytes) {
|
||||
const sizeInKB = sizeInBytes / 1024; // 1 KB = 1024 bytes
|
||||
if (sizeInKB < 1024) {
|
||||
return `${sizeInKB.toFixed(2)} KB`;
|
||||
} else {
|
||||
const sizeInMB = sizeInKB / 1024; // 1 MB = 1024 KB
|
||||
if (sizeInMB < 1024) {
|
||||
return `${sizeInMB.toFixed(2)} MB`;
|
||||
} else {
|
||||
const sizeInGB = sizeInMB / 1024; // 1 GB = 1024 MB
|
||||
if (sizeInGB < 1024) {
|
||||
return `${sizeInGB.toFixed(2)} GB`;
|
||||
} else {
|
||||
const sizeInTB = sizeInGB / 1024; // 1 TB = 1024 GB
|
||||
if (sizeInTB < 1024) {
|
||||
return `${sizeInTB.toFixed(2)} TB`;
|
||||
} else {
|
||||
const sizeInPB = sizeInTB / 1024; // 1 PB = 1024 TB
|
||||
return `${sizeInPB.toFixed(2)} PB`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function formatTotalWatchTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600); // 1 hour = 3600 seconds
|
||||
const minutes = Math.floor((seconds % 3600) / 60); // 1 minute = 60 seconds
|
||||
@@ -83,6 +110,16 @@ function LibraryCard(props) {
|
||||
<Col className="text-end">{props.data.CollectionType==='tvshows' ? 'Series' : "Movies"}</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="space-between-end card-row">
|
||||
<Col className="card-label">Total Files</Col>
|
||||
<Col className="text-end">{props.metadata.files}</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="space-between-end card-row">
|
||||
<Col className="card-label">Library Size</Col>
|
||||
<Col className="text-end">{formatFileSize(props.metadata.Size)}</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="space-between-end card-row">
|
||||
<Col className="card-label">Total Plays</Col>
|
||||
<Col className="text-end">{props.data.Plays}</Col>
|
||||
@@ -117,6 +154,7 @@ function LibraryCard(props) {
|
||||
<Col className="card-label">Episodes</Col>
|
||||
<Col className="text-end">{props.data.CollectionType==='tvshows' ? props.data.Episode_Count : ''}</Col>
|
||||
</Row>
|
||||
|
||||
</Card.Body>
|
||||
|
||||
</Card>
|
||||
|
||||
@@ -91,7 +91,9 @@ function sessionCard(props) {
|
||||
<Row className="justify-content-between">
|
||||
<Col>
|
||||
<Card.Text>
|
||||
{props.data.session.NowPlayingItem.SeriesName ? (props.data.session.NowPlayingItem.SeriesName+" - "+ props.data.session.NowPlayingItem.Name) : (props.data.session.NowPlayingItem.Name)}
|
||||
<Link to={`/item/${props.data.session.NowPlayingItem.Id}`} target="_blank">
|
||||
{props.data.session.NowPlayingItem.SeriesName ? (props.data.session.NowPlayingItem.SeriesName+" - "+ props.data.session.NowPlayingItem.Name) : (props.data.session.NowPlayingItem.Name)}
|
||||
</Link>
|
||||
</Card.Text>
|
||||
</Col>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function LibrarySync() {
|
||||
return (
|
||||
<div className="settings-form">
|
||||
<h1 className="my-2">Tasks</h1>
|
||||
<Row className="mb-3" controlId="form_task_sync">
|
||||
<Row className="mb-3">
|
||||
|
||||
<Form.Label column sm="2">
|
||||
Syncronize with Jellyfin
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function SettingsConfig() {
|
||||
<div className="general-settings-page">
|
||||
<h1>General Settings</h1>
|
||||
<Form onSubmit={handleFormSubmit} className="settings-form">
|
||||
<Form.Group as={Row} className="mb-3" controlId="form_jellyfin_url">
|
||||
<Form.Group as={Row} className="mb-3" >
|
||||
<Form.Label column className="fs-4">
|
||||
Jellyfin Url
|
||||
</Form.Label>
|
||||
@@ -131,7 +131,7 @@ export default function SettingsConfig() {
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group as={Row} className="mb-3" controlId="form_api_key">
|
||||
<Form.Group as={Row} className="mb-3">
|
||||
<Form.Label column className="fs-4">
|
||||
API Key
|
||||
</Form.Label>
|
||||
|
||||
@@ -17,7 +17,7 @@ function ItemStatComponent(props) {
|
||||
const cardStyle = {
|
||||
backgroundImage: `url(${props.base_url}/Items/${props.data[0].Id}/Images/Backdrop/?fillWidth=300&quality=10), linear-gradient(to right, #00A4DC, #AA5CC3)`,
|
||||
height:'100%',
|
||||
backgroundSize: 'cover',
|
||||
backgroundSize: 'contain',
|
||||
};
|
||||
|
||||
const cardBgStyle = {
|
||||
@@ -33,9 +33,9 @@ function ItemStatComponent(props) {
|
||||
|
||||
|
||||
return (
|
||||
<Card className="stat-card" style={cardStyle}>
|
||||
<div style={cardBgStyle}>
|
||||
<Row className="h-100">
|
||||
<Card className="stat-card rounded-2" style={cardStyle}>
|
||||
<div style={cardBgStyle} className="rounded-2">
|
||||
<Row className="h-100 rounded-2">
|
||||
<Col className="d-none d-lg-block stat-card-banner">
|
||||
{props.icon ?
|
||||
<div className="stat-card-icon">
|
||||
@@ -49,8 +49,7 @@ function ItemStatComponent(props) {
|
||||
</div>
|
||||
)}
|
||||
<Card.Img
|
||||
variant="top"
|
||||
className="stat-card-image rounded-0"
|
||||
className="stat-card-image"
|
||||
src={props.base_url + "/Items/" + props.data[0].Id + "/Images/Primary?fillWidth=400&quality=90"}
|
||||
style={{ display: loaded ? 'block' : 'none' }}
|
||||
onLoad={handleImageLoad}
|
||||
@@ -81,7 +80,17 @@ function ItemStatComponent(props) {
|
||||
<Card.Text>{item.Name}</Card.Text>
|
||||
</Link>
|
||||
:
|
||||
<Card.Text>{item.Name || item.Client}</Card.Text>
|
||||
!item.Client && !props.icon ?
|
||||
<Link to={`/item/${item.Id}`}>
|
||||
<Card.Text>{item.Name}</Card.Text>
|
||||
</Link>
|
||||
:
|
||||
!item.Client && props.icon ?
|
||||
<Link to={`/libraries/${item.Id}`}>
|
||||
<Card.Text>{item.Name}</Card.Text>
|
||||
</Link>
|
||||
:
|
||||
<Card.Text>{item.Client}</Card.Text>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ function DailyPlayStats(props) {
|
||||
|
||||
const [stats, setStats] = useState();
|
||||
const [libraries, setLibraries] = useState();
|
||||
const [days, setDays] = useState(15);
|
||||
const [days, setDays] = useState(20);
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function DailyPlayStats(props) {
|
||||
}
|
||||
return (
|
||||
<div className="main-widget">
|
||||
<h1>Daily Play Count Per Library - Last {days} Days</h1>
|
||||
<h2 className="text-start my-2">Daily Play Count Per Library - Last {days} Days</h2>
|
||||
|
||||
<div className="graph">
|
||||
<Chart libraries={libraries} stats={stats} />
|
||||
|
||||
@@ -7,7 +7,7 @@ import "../../css/stats.css";
|
||||
function PlayStatsByDay(props) {
|
||||
const [stats, setStats] = useState();
|
||||
const [libraries, setLibraries] = useState();
|
||||
const [days, setDays] = useState(60);
|
||||
const [days, setDays] = useState(20);
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +62,7 @@ function PlayStatsByDay(props) {
|
||||
|
||||
return (
|
||||
<div className="statistics-widget">
|
||||
<h1>Play Count By Day - Last {days} Days</h1>
|
||||
<h2 className="text-start my-2">Play Count By Day - Last {days} Days</h2>
|
||||
<div className="graph small">
|
||||
<Chart libraries={libraries} stats={stats} />
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import "../../css/stats.css";
|
||||
function PlayStatsByHour(props) {
|
||||
const [stats, setStats] = useState();
|
||||
const [libraries, setLibraries] = useState();
|
||||
const [days, setDays] = useState(60);
|
||||
const [days, setDays] = useState(20);
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,7 +62,7 @@ function PlayStatsByHour(props) {
|
||||
|
||||
return (
|
||||
<div className="statistics-widget">
|
||||
<h1>Play Count By Hour - Last {days} Days</h1>
|
||||
<h2 className="text-start my-2">Play Count By Hour - Last {days} Days</h2>
|
||||
<div className="graph small">
|
||||
<Chart libraries={libraries} stats={stats} />
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ function GlobalStats(props) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>User Stats</h1>
|
||||
<h1 className="py-3">User Stats</h1>
|
||||
<div className="global-stats-container">
|
||||
<WatchTimeStats data={dayStats} heading={"Last 24 Hours"} />
|
||||
<WatchTimeStats data={weekStats} heading={"Last 7 Days"} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
import LastPlayedItem from "./lastplayed/last-played-item";
|
||||
import LastWatchedCard from "../general/last-watched-card";
|
||||
|
||||
import Config from "../../../lib/config";
|
||||
import "../../css/users/user-details.css";
|
||||
@@ -23,6 +23,8 @@ function LastPlayed(props) {
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if(config)
|
||||
{
|
||||
try {
|
||||
const itemData = await axios.post(`/stats/getUserLastPlayed`, {
|
||||
userid: props.UserId,
|
||||
@@ -35,7 +37,8 @@ function LastPlayed(props) {
|
||||
setData(itemData.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
@@ -57,10 +60,10 @@ function LastPlayed(props) {
|
||||
|
||||
return (
|
||||
<div className="last-played">
|
||||
<h1>Last Watched</h1>
|
||||
<h1 className="my-3">Last Watched</h1>
|
||||
<div className="last-played-container">
|
||||
{data.map((item) => (
|
||||
<LastPlayedItem data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
|
||||
<LastWatchedCard data={item} base_url={config.hostUrl} key={item.Id+item.EpisodeNumber}/>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import React, {useState} from "react";
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
|
||||
function formatTime(time) {
|
||||
|
||||
const units = {
|
||||
days: ['Day', 'Days'],
|
||||
hours: ['Hour', 'Hours'],
|
||||
minutes: ['Minute', 'Minutes'],
|
||||
seconds: ['Second', 'Seconds']
|
||||
};
|
||||
|
||||
let formattedTime = '';
|
||||
|
||||
if (time.days) {
|
||||
formattedTime = `${time.days} ${units.days[time.days > 1 ? 1 : 0]}`;
|
||||
} else if (time.hours) {
|
||||
formattedTime = `${time.hours} ${units.hours[time.hours > 1 ? 1 : 0]}`;
|
||||
} else if (time.minutes) {
|
||||
formattedTime = `${time.minutes} ${units.minutes[time.minutes > 1 ? 1 : 0]}`;
|
||||
} else {
|
||||
formattedTime = `${time.seconds} ${units.seconds[time.seconds > 1 ? 1 : 0]}`;
|
||||
}
|
||||
|
||||
return `${formattedTime} ago`;
|
||||
}
|
||||
|
||||
|
||||
function LastPlayedItem(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
return (
|
||||
<div className="last-card">
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : <Blurhash hash={props.data.PrimaryImageHash} width={'100%'} height={'100%'}/>}
|
||||
<img
|
||||
src={
|
||||
`${
|
||||
props.base_url +
|
||||
"/Items/" +
|
||||
props.data.Id +
|
||||
"/Images/Primary?fillHeight=320&fillWidth=213&quality=50"}`
|
||||
}
|
||||
onLoad={() => setLoaded(true)}
|
||||
alt=""
|
||||
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="last-item-details">
|
||||
<div className="last-last-played">
|
||||
{formatTime(props.data.LastPlayed)}
|
||||
</div>
|
||||
<div className="last-item-name"> {props.data.Name}</div>
|
||||
<div className="last-item-episode"> {props.data.EpisodeName}</div>
|
||||
</div>
|
||||
{props.data.SeasonNumber ?
|
||||
<div className="last-item-episode number"> S{props.data.SeasonNumber} - E{props.data.EpisodeNumber}</div>:
|
||||
<></>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LastPlayedItem;
|
||||
@@ -22,6 +22,7 @@ function UserDetails(props) {
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if(config){
|
||||
try {
|
||||
const userData = await axios.post(`/stats/getUserDetails`, {
|
||||
userid: props.UserId,
|
||||
@@ -35,6 +36,8 @@ function UserDetails(props) {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
|
||||
@@ -9,7 +9,7 @@ div a
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.table-rows-content:hover a
|
||||
.table-rows-content div:hover a
|
||||
{
|
||||
color: #00A4DC;
|
||||
}
|
||||
|
||||
22
src/pages/css/items/item-details.css
Normal file
22
src/pages/css/items/item-details.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.item-detail-container
|
||||
{
|
||||
color:white;
|
||||
background-color: rgb(100, 100, 100,0.2);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.item-name
|
||||
{
|
||||
font-size: 2.5em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.item-image
|
||||
{
|
||||
max-width: 200px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
background-color: rgb(100, 100, 100,0.2);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
min-height: 300px;
|
||||
@@ -46,13 +45,27 @@
|
||||
|
||||
}
|
||||
|
||||
.episode{
|
||||
width: 220px !important;
|
||||
height: 128px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last-card-banner {
|
||||
width: 150px;
|
||||
height: 224px;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last-card-banner:hover {
|
||||
opacity: 0.5;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.last-card-banner img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
background-color: #1e1c22;
|
||||
}
|
||||
.component-loading {
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@
|
||||
.small
|
||||
{
|
||||
height: 500px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.statistics-graphs
|
||||
{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
grid-gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
|
||||
@@ -42,12 +43,6 @@
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.statistics-widget
|
||||
{
|
||||
flex: 1;
|
||||
max-width: 49.5%;
|
||||
/* margin-right: 20px; */
|
||||
}
|
||||
|
||||
.statistics-widget h1{
|
||||
margin-bottom: 10px !important;
|
||||
@@ -57,13 +52,13 @@
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.chart-canvas {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.chart-canvas {
|
||||
height: 600px;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.Users
|
||||
{
|
||||
color: white;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 50px;
|
||||
margin-top: 10px;
|
||||
/* padding-right: 20px; */
|
||||
padding-bottom: 20px;
|
||||
/* margin-top: 10px; */
|
||||
}
|
||||
|
||||
.user-activity-table {
|
||||
|
||||
@@ -13,6 +13,7 @@ import Row from "react-bootstrap/Row";
|
||||
|
||||
function Libraries() {
|
||||
const [data, setData] = useState();
|
||||
const [metadata, setMetaData] = useState();
|
||||
const [config, setConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,9 +40,23 @@ function Libraries() {
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("data");
|
||||
setData(data.data);
|
||||
console.log(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
const metadataurl = `/stats/getLibraryMetadata`;
|
||||
|
||||
axios
|
||||
.get(metadataurl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
setMetaData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
@@ -54,15 +69,12 @@ function Libraries() {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
fetchLibraries();
|
||||
}
|
||||
|
||||
fetchLibraries();
|
||||
const intervalId = setInterval(fetchLibraries, 60000 * 60);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config]);
|
||||
}, [ config]);
|
||||
|
||||
if (!data || !config) {
|
||||
if (!data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
@@ -74,7 +86,7 @@ function Libraries() {
|
||||
{data &&
|
||||
data.map((item) => (
|
||||
|
||||
<LibraryCard key={item.Id} data={item} base_url={config.hostUrl}/>
|
||||
<LibraryCard key={item.Id} data={item} metadata={metadata.find(data => data.Id === item.Id)} base_url={config.hostUrl}/>
|
||||
|
||||
|
||||
))}
|
||||
|
||||
@@ -8,8 +8,8 @@ import PlayStatsByDay from "./components/statistics/play-stats-by-day";
|
||||
import PlayStatsByHour from "./components/statistics/play-stats-by-hour";
|
||||
|
||||
function Statistics() {
|
||||
const [days, setDays] = useState(60);
|
||||
const [input, setInput] = useState(60);
|
||||
const [days, setDays] = useState(20);
|
||||
const [input, setInput] = useState(20);
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
|
||||
@@ -99,7 +99,6 @@ function Users() {
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
setData(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -140,7 +139,7 @@ function Users() {
|
||||
|
||||
return (
|
||||
<div className="Users">
|
||||
<div className="Heading">
|
||||
<div className="Heading py-2">
|
||||
<h1 >All Users</h1>
|
||||
<div className="pagination-range">
|
||||
<div className="header">Items</div>
|
||||
|
||||
Reference in New Issue
Block a user