backend changes to improve sync

Reworked library view script,this reduces page load time by 90%
catered for new episodes in recently added feed.
Underlying work done for toggle to untrack certain libraries
General ui fixes
Backup files now limited to latest 5 files
Updated compose to have a limit on log files and sizes (Thanks @Hutch79)
This commit is contained in:
Thegan Govender
2023-06-24 23:56:58 +02:00
parent 364aa2ac09
commit 1fc57445cd
18 changed files with 547 additions and 134 deletions

View File

@@ -180,10 +180,6 @@ 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}'`;
@@ -802,6 +798,92 @@ router.post("/getLibraries", async (req, res) => {
}
});
router.get('/TrackedLibraries', async(req, res) => {
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
if (config[0].JF_HOST === null || config[0].JF_API_KEY === null) {
res.send({ error: "Config Details Not Found" });
return;
}
let url=`${config[0].JF_HOST}/Library/MediaFolders`;
const response_data = await axios_instance.get(url, {
headers: {
"X-MediaBrowser-Token": config[0].JF_API_KEY ,
},
});
const filtered_items=response_data.data.Items.filter((type) => !["boxsets","playlists"].includes(type.CollectionType))
const excluded_libraries=await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if(excluded_libraries.length>0)
{
const libraries =excluded_libraries[0].settings?.ExcludedLibraries||[];
const librariesWithTrackedStatus = filtered_items.map((items) => ({
...items,
...{ Tracked: !libraries.includes(items.Id)},
}));
res.send(librariesWithTrackedStatus);
}else
{
res.status(404);
res.send({ error: "Settings Not Found" });
}
});
router.post('/setExcludedLibraries', async(req, res) => {
const { libraryID } = req.body;
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 || !libraryID) {
res.status(404);
res.send({ error: "Config Details Not Found" });
return;
}
const settingsjson=await db.query('SELECT settings FROM app_config where "ID"=1').then((res) => res.rows);
if(settingsjson.length>0)
{
const settings =settingsjson[0].settings||{};
let libraries=settings.ExcludedLibraries||[];
if(libraries.includes(libraryID))
{
libraries = libraries.filter(item => item !== libraryID);
}else{
libraries.push(libraryID);
}
settings.ExcludedLibraries=libraries;
let query='UPDATE app_config SET settings=$1 where "ID"=1';
const { rows } = await db.query(
query,
[settings]
);
res.send("Settings updated succesfully");
}
});

View File

@@ -17,6 +17,7 @@ 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';
const backupfolder='backup-data';
// Tables to back up
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity','jf_playback_reporting_plugin_data','jf_item_info'];
@@ -48,15 +49,15 @@ async function backup(refLog) {
try{
let now = moment();
const backupfolder='./backup-data';
const backuppath='./'+backupfolder;
if (!fs.existsSync(backupfolder)) {
fs.mkdirSync(backupfolder);
if (!fs.existsSync(backuppath)) {
fs.mkdirSync(backuppath);
console.log('Directory created successfully!');
}
if (!checkFolderWritePermission(backupfolder)) {
console.error('No write permissions for the folder:', backupfolder);
refLog.logData.push({ color: "red", Message: "Backup Failed: No write permissions for the folder: "+backupfolder });
if (!checkFolderWritePermission(backuppath)) {
console.error('No write permissions for the folder:', backuppath);
refLog.logData.push({ color: "red", Message: "Backup Failed: No write permissions for the folder: "+backuppath });
refLog.logData.push({ color: "red", Message: "Backup Failed with errors"});
refLog.result='Failed';
await pool.end();
@@ -79,7 +80,6 @@ async function backup(refLog) {
const query = `SELECT * FROM ${table}`;
const { rows } = await pool.query(query);
console.log(`Reading ${rows.length} rows for table ${table}`);
refLog.logData.push({color: "dodgerblue",Message: `Saving ${rows.length} rows for table ${table}`});
backup_data.push({[table]:rows});
@@ -90,6 +90,53 @@ async function backup(refLog) {
await stream.write(JSON.stringify(backup_data));
stream.end();
refLog.logData.push({ color: "lawngreen", Message: "Backup Complete" });
refLog.logData.push({ color: "dodgerblue", Message: "Removing old backups" });
//Cleanup excess backups
let deleteCount=0;
const directoryPath = path.join(__dirname, backupfolder);
const files = await new Promise((resolve, reject) => {
fs.readdir(directoryPath, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
});
});
let fileData = files.filter(file => file.endsWith('.json'))
.map(file => {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
return {
name: file,
size: stats.size,
datecreated: stats.birthtime
};
});
fileData = fileData.sort((a, b) => new Date(b.datecreated) - new Date(a.datecreated)).slice(5);
for (var oldBackup of fileData) {
const oldBackupFile = path.join(__dirname, backupfolder, oldBackup.name);
await new Promise((resolve, reject) => {
fs.unlink(oldBackupFile, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
deleteCount += 1;
refLog.logData.push({ color: "yellow", Message: `${oldBackupFile} has been deleted.` });
}
refLog.logData.push({ color: "lawngreen", Message: deleteCount+" backups removed." });
}catch(error)
{
@@ -100,6 +147,8 @@ async function backup(refLog) {
await pool.end();
}
// Restore function
@@ -259,8 +308,7 @@ router.get('/restore/:filename', async (req, res) => {
Logging.insertLog(log);
});
//list backup files
const backupfolder='backup-data';
router.get('/files', (req, res) => {

View File

@@ -36,34 +36,29 @@ async function deleteBulk(table_name, data) {
try {
await client.query('BEGIN');
// const AllIds = data.map((row) => row.Id);
if (data.length !== 0) {
if (data && data.length !== 0) {
const deleteQuery = {
text: `DELETE FROM ${table_name} WHERE "Id" IN (${pgp.as.csv(
data
)})`
};
// console.log(deleteQuery);
// console.log(deleteQuery);
await client.query(deleteQuery);
}
// else {
// await client.query(`DELETE FROM ${table_name}`);
// console.log('Delete All');
// }
await client.query('COMMIT');
message=(data.length + " Rows removed.");
} catch (error) {
await client.query('ROLLBACK');
message=(''+ error);
message=('Bulk delete error: '+ error);
result='ERROR';
} finally {
client.release();
}
return ({Result:result,message:'Bulk delete error:'+message});
return ({Result:result,message:''+message});
}
async function insertBulk(table_name, data,columns) {

View File

@@ -4,7 +4,7 @@ exports.up = async function(knex) {
const hasTable = await knex.schema.hasTable('app_config');
if (hasTable) {
await knex.schema.alterTable('app_config', function(table) {
table.json('settings').defaultTo({settings:{time_format:'12hr'}});
table.json('settings').defaultTo({time_format:'12hr'});
});
}
}catch (error) {

View File

@@ -0,0 +1,125 @@
exports.up = function(knex) {
const query = `
CREATE OR REPLACE VIEW public.js_library_stats_overview
AS
SELECT DISTINCT ON (l."Id") l."Id",
l."Name",
l."ServerId",
l."IsFolder",
l."Type",
l."CollectionType",
l."ImageTagsPrimary",
i."Id" AS "ItemId",
i."Name" AS "ItemName",
i."Type" AS "ItemType",
i."PrimaryImageHash",
s."IndexNumber" AS "SeasonNumber",
e."IndexNumber" AS "EpisodeNumber",
e."Name" AS "EpisodeName",
( SELECT count(*) AS count
FROM jf_playback_activity a
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
WHERE i_1."ParentId" = l."Id") AS "Plays",
( SELECT sum(a."PlaybackDuration") AS sum
FROM jf_playback_activity a
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
WHERE i_1."ParentId" = l."Id") AS total_playback_duration,
l.total_play_time::numeric AS total_play_time,
l.item_count AS "Library_Count",
l.season_count AS "Season_Count",
l.episode_count AS "Episode_Count",
now() - latest_activity."ActivityDateInserted" AS "LastActivity"
FROM jf_libraries l
LEFT JOIN ( SELECT DISTINCT ON (i_1."ParentId") jf_playback_activity."Id",
jf_playback_activity."IsPaused",
jf_playback_activity."UserId",
jf_playback_activity."UserName",
jf_playback_activity."Client",
jf_playback_activity."DeviceName",
jf_playback_activity."DeviceId",
jf_playback_activity."ApplicationVersion",
jf_playback_activity."NowPlayingItemId",
jf_playback_activity."NowPlayingItemName",
jf_playback_activity."SeasonId",
jf_playback_activity."SeriesName",
jf_playback_activity."EpisodeId",
jf_playback_activity."PlaybackDuration",
jf_playback_activity."ActivityDateInserted",
jf_playback_activity."PlayMethod",
i_1."ParentId"
FROM jf_playback_activity
JOIN jf_library_items i_1 ON i_1."Id" = jf_playback_activity."NowPlayingItemId"
ORDER BY i_1."ParentId", jf_playback_activity."ActivityDateInserted" DESC) latest_activity ON l."Id" = latest_activity."ParentId"
LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
`;
return knex.schema.raw(query).catch(function(error) {
console.error(error);
});
};
exports.down = function(knex) {
return knex.schema.raw(`
CREATE OR REPLACE VIEW public.js_library_stats_overview
AS
SELECT DISTINCT ON (l."Id") l."Id",
l."Name",
l."ServerId",
l."IsFolder",
l."Type",
l."CollectionType",
l."ImageTagsPrimary",
i."Id" AS "ItemId",
i."Name" AS "ItemName",
i."Type" AS "ItemType",
i."PrimaryImageHash",
s."IndexNumber" AS "SeasonNumber",
e."IndexNumber" AS "EpisodeNumber",
e."Name" AS "EpisodeName",
( SELECT count(*) AS count
FROM jf_playback_activity a
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
WHERE i_1."ParentId" = l."Id") AS "Plays",
( SELECT sum(a."PlaybackDuration") AS sum
FROM jf_playback_activity a
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
WHERE i_1."ParentId" = l."Id") AS total_playback_duration,
l.total_play_time::numeric AS total_play_time,
l.item_count AS "Library_Count",
l.season_count AS "Season_Count",
l.episode_count AS "Episode_Count",
now() - latest_activity."ActivityDateInserted" AS "LastActivity"
FROM jf_libraries l
LEFT JOIN jf_library_count_view cv ON cv."Id" = l."Id"
LEFT JOIN ( SELECT jf_playback_activity."Id",
jf_playback_activity."IsPaused",
jf_playback_activity."UserId",
jf_playback_activity."UserName",
jf_playback_activity."Client",
jf_playback_activity."DeviceName",
jf_playback_activity."DeviceId",
jf_playback_activity."ApplicationVersion",
jf_playback_activity."NowPlayingItemId",
jf_playback_activity."NowPlayingItemName",
jf_playback_activity."SeasonId",
jf_playback_activity."SeriesName",
jf_playback_activity."EpisodeId",
jf_playback_activity."PlaybackDuration",
jf_playback_activity."ActivityDateInserted",
jf_playback_activity."PlayMethod",
i_1."ParentId"
FROM jf_playback_activity
JOIN jf_library_items i_1 ON i_1."Id" = jf_playback_activity."NowPlayingItemId"
ORDER BY jf_playback_activity."ActivityDateInserted" DESC) latest_activity ON l."Id" = latest_activity."ParentId"
LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;
`);
};

View File

@@ -144,6 +144,7 @@ router.get('/Items/Images/Backdrop/', async(req, res) => {
});
});
module.exports = router;

View File

@@ -370,6 +370,8 @@ router.get("/getRecentlyAdded", async (req, res) => {
{
url+=`?parentId=${libraryid}`;
}
const response_data = await axios_instance.get(url, {
headers: {

View File

@@ -271,21 +271,20 @@ async function syncLibraryFolders(refLog,data)
{
try
{
const existingIds = await db
.query('SELECT "Id" FROM jf_libraries')
.then((res) => res.rows.map((row) => row.Id));
let dataToInsert = [];
//filter fix if jf_libraries is empty
if (existingIds.length === 0) {
dataToInsert = await data.map(jf_libraries_mapping);
} else {
dataToInsert = await data.filter((row) => !existingIds.includes(row.Id)).map(jf_libraries_mapping);
}
if (dataToInsert.length !== 0) {
let result = await db.insertBulk("jf_libraries",dataToInsert,jf_libraries_columns);
if (result.Result === "SUCCESS") {
@@ -299,12 +298,20 @@ async function syncLibraryFolders(refLog,data)
}
}
const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id ));
//----------------------DELETE FUNCTION
//GET EPISODES IN SEASONS
//GET SEASONS IN SHOWS
//GET SHOWS IN LIBRARY
//FINALY DELETE LIBRARY
if (toDeleteIds.length > 0) {
const ItemsToDelete=await db.query(`SELECT "Id" FROM jf_library_items where "ParentId" in (${toDeleteIds.map(id => `'${id}'`).join(',')})`).then((res) => res.rows.map((row) => row.Id));
let resultItem=await db.deleteBulk("jf_library_items",ItemsToDelete);
console.log(resultItem.message);
let result = await db.deleteBulk("jf_libraries",toDeleteIds);
if (result.Result === "SUCCESS") {
refLog.loggedData.push(toDeleteIds.length + " Rows Removed.");
} else {
refLog.loggedData.push({color: "red",Message: "Error: "+result.message,});
refLog.result='Failed';
}
@@ -322,10 +329,14 @@ async function syncLibraryItems(refLog,data)
{
try{
let existingLibraryIds = await db
.query('SELECT "Id" FROM jf_libraries')
.then((res) => res.rows.map((row) => row.Id));
refLog.loggedData.push({ color: "lawngreen", Message: "Syncing... 1/3" });
refLog.loggedData.push({color: "yellow",Message: "Beginning Library Item Sync",});
data=data.filter((row) => existingLibraryIds.includes(row.ParentId));
let insertMessage='';
let deleteCounter = 0;
@@ -343,7 +354,7 @@ async function syncLibraryItems(refLog,data)
if (dataToInsert.length !== 0) {
let result = await db.insertBulk("jf_library_items",dataToInsert,jf_library_items_columns);
if (result.Result === "SUCCESS") {
insertMessage = `${dataToInsert.length-existingIds.length} Rows Inserted. ${existingIds.length} Rows Updated.`;
insertMessage = `${dataToInsert.length-existingIds.length >0 ? dataToInsert.length-existingIds.length : 0} Rows Inserted. ${existingIds.length} Rows Updated.`;
} else {
refLog.loggedData.push({
color: "red",
@@ -488,9 +499,9 @@ async function syncShowItems(refLog,data)
}
refLog.loggedData.push({color: "dodgerblue",Message: `Seasons: ${insertSeasonsCount} Rows Inserted. ${updateSeasonsCount} Rows Updated.`});
refLog.loggedData.push({color: "dodgerblue",Message: `Seasons: ${insertSeasonsCount > 0 ? insertSeasonsCount : 0} Rows Inserted. ${updateSeasonsCount} Rows Updated.`});
refLog.loggedData.push({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
refLog.loggedData.push({color: "dodgerblue",Message: `Episodes: ${insertEpisodeCount} Rows Inserted. ${updateEpisodeCount} Rows Updated.`});
refLog.loggedData.push({color: "dodgerblue",Message: `Episodes: ${insertEpisodeCount > 0 ? insertEpisodeCount : 0} Rows Inserted. ${updateEpisodeCount} Rows Updated.`});
refLog.loggedData.push({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
refLog.loggedData.push({ color: "yellow", Message: "Sync Complete" });
}catch(error)
@@ -605,9 +616,9 @@ async function syncItemInfo(refLog)
// console.log(Episode.Name)
}
refLog.loggedData.push({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted. "+updateItemInfoCount +" Item Info Updated"});
refLog.loggedData.push({color: "dodgerblue",Message: (insertItemInfoCount >0 ? insertItemInfoCount : 0) + " Item Info inserted. "+updateItemInfoCount +" Item Info Updated"});
refLog.loggedData.push({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",});
refLog.loggedData.push({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes Info inserted. "+updateEpisodeInfoCount +" Episodes Info Updated"});
refLog.loggedData.push({color: "dodgerblue",Message: (insertEpisodeInfoCount > 0 ? insertEpisodeInfoCount:0) + " Episodes Info inserted. "+updateEpisodeInfoCount +" Episodes Info Updated"});
refLog.loggedData.push({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Info Removed.",});
refLog.loggedData.push({ color: "lawngreen", Message: "Info Sync Complete" });
}catch(error)
@@ -719,7 +730,7 @@ async function updateLibraryStatsData(refLog)
}
async function fullSync()
async function fullSync(taskType)
{
try
{
@@ -773,7 +784,7 @@ async function fullSync()
"Id":uuid,
"Name":"Jellyfin Sync",
"Type":"Task",
"ExecutionType":"Automatic",
"ExecutionType":taskType,
"Duration":diffInSeconds,
"TimeRun":startTime,
"Log":JSON.stringify(refLog.loggedData),
@@ -796,66 +807,14 @@ async function fullSync()
///////////////////////////////////////Sync All
router.get("/beingSync", async (req, res) => {
// socket.clearMessages();
let refLog={loggedData:[],result:'Success'};
let startTime = moment();
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
res.send({ error: "Config Details Not Found" });
refLog.loggedData.push({ Message: "Error: Config details not found!" });
refLog.result='Failed';
return;
}
const _sync = new sync(rows[0].JF_HOST, rows[0].JF_API_KEY);
const admins = await _sync.getAdminUser(refLog);
const userid = admins[0].Id;
const libraries = await _sync.getItems('userid',userid,{recursive:false}); //getting all root folders aka libraries + items
const data=[];
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
for (let i = 0; i < libraries.length; i++) {
const item = libraries[i];
let libraryItems = await _sync.getItems('parentId',item.Id);
const libraryItemsWithParent = libraryItems.map((items) => ({
...items,
...{ ParentId: item.Id },
}));
data.push(...libraryItemsWithParent);
}
const library_items=data.filter((item) => ['Movie','Audio','Series'].includes(item.Type));
const seasons_and_episodes=data.filter((item) => ['Season','Episode'].includes(item.Type));
await syncUserData(refLog);
await syncLibraryFolders(refLog,libraries);
await syncLibraryItems(refLog,library_items);
await syncShowItems(refLog,seasons_and_episodes);
await syncItemInfo(refLog);
await updateLibraryStatsData(refLog);
await removeOrphanedData(refLog);
const uuid = randomUUID();
let endTime = moment();
let diffInSeconds = endTime.diff(startTime, 'seconds');
const log=
{
"Id":uuid,
"Name":"Jellyfin Sync",
"Type":"Task",
"ExecutionType":"Manual",
"Duration":diffInSeconds,
"TimeRun":startTime,
"Log":JSON.stringify(refLog.loggedData),
"Result":refLog.result
};
logging.insertLog(log);
await fullSync('Manual');
res.send();
});

View File

@@ -38,7 +38,7 @@ async function intervalCallback() {
console.log('Running Scheduled Sync');
await sync.fullSync();
await sync.fullSync('Automatic');
console.log('Scheduled Sync Complete');
} catch (error)

View File

@@ -18,4 +18,9 @@ services:
depends_on:
- jellystat-db
networks:
default:
default:
logging:
driver: "json-file"
options:
max-file: "5" # number of files or file count
max-size: "10m" # file size

View File

@@ -0,0 +1,99 @@
import React, {useState} from "react";
import axios from "axios";
import "../../css/library/library-card.css";
import { Form ,Card,Row,Col } from 'react-bootstrap';
import TvLineIcon from "remixicon-react/TvLineIcon";
import FilmLineIcon from "remixicon-react/FilmLineIcon";
import FileMusicLineIcon from "remixicon-react/FileMusicLineIcon";
import CheckboxMultipleBlankLineIcon from "remixicon-react/CheckboxMultipleBlankLineIcon";
function SelectionCard(props) {
const [imageLoaded, setImageLoaded] = useState(true);
const [checked, setChecked] = useState(props.data.Tracked);
const SeriesIcon=<TvLineIcon size={"50%"} color="white"/> ;
const MovieIcon=<FilmLineIcon size={"50%"} color="white"/> ;
const MusicIcon=<FileMusicLineIcon size={"50%"} color="white"/> ;
const MixedIcon=<CheckboxMultipleBlankLineIcon size={"50%"} color="white"/> ;
const token = localStorage.getItem('token');
const default_image=<div className="default_library_image d-flex justify-content-center align-items-center">{props.data.CollectionType==='tvshows' ? SeriesIcon : props.data.CollectionType==='movies'? MovieIcon : props.data.CollectionType==='music'? MusicIcon : MixedIcon} </div>;
const handleChange = async () => {
await axios
.post("/api/setExcludedLibraries", {
libraryID:props.data.Id
}, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
.then(()=>
{
setChecked(!checked);
})
.catch((error) => {
console.error(error);
});
};
return (
<Card className="bg-transparent lib-card rounded-3">
<div className="library-card-image">
{imageLoaded?
<Card.Img
variant="top"
className="library-card-banner default_library_image"
src={"/proxy/Items/Images/Primary?id=" + props.data.Id + "&fillWidth=800&quality=50"}
onError={() =>setImageLoaded(false)}
/>
:
default_image
}
</div>
<Card.Body className="library-card-details rounded-bottom">
<Row className="space-between-end card-row">
<Col className="card-label">Library</Col>
<Col className="text-end">{props.data.Name}</Col>
</Row>
<Row className="space-between-end card-row">
<Col className="card-label">Type</Col>
<Col className="text-end">{props.data.CollectionType==='tvshows' ? 'Series' : props.data.CollectionType==='movies'? "Movies" : props.data.CollectionType==='music'? "Music" : 'Mixed'}</Col>
</Row>
<Row className="space-between-end card-row">
<Col className="card-label">Tracked</Col>
<Col className="text-end">
<Form>
<Form.Check
type="switch"
id="tracker-switch"
checked={checked}
onChange={handleChange}
/>
</Form>
</Col>
</Row>
</Card.Body>
</Card>
);
}
export default SelectionCard;

View File

@@ -105,6 +105,7 @@ function Row(data) {
</IconButton>
</TableCell>
<TableCell><Link to={`/users/${row.UserId}`} className='text-decoration-none'>{row.UserName}</Link></TableCell>
<TableCell>{row.RemoteEndPoint || '-'}</TableCell>
<TableCell><Link to={`/libraries/item/${row.EpisodeId || row.NowPlayingItemId}`} className='text-decoration-none'>{!row.SeriesName ? row.NowPlayingItemName : row.SeriesName+' - '+ row.NowPlayingItemName}</Link></TableCell>
<TableCell className='activity-client' ><span onClick={()=>openModal(row)}>{row.Client}</span></TableCell>
<TableCell>{Intl.DateTimeFormat('en-UK', options).format(new Date(row.ActivityDateInserted))}</TableCell>
@@ -112,7 +113,7 @@ function Row(data) {
<TableCell>{row.results.length !==0 ? row.results.length : 1}</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={7}>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
@@ -120,6 +121,7 @@ function Row(data) {
<TableHead>
<TableRow>
<TableCell>User</TableCell>
<TableCell>IP Address</TableCell>
<TableCell>Title</TableCell>
<TableCell>Client</TableCell>
<TableCell>Date</TableCell>
@@ -131,6 +133,7 @@ function Row(data) {
{row.results.sort((a, b) => new Date(b.ActivityDateInserted) - new Date(a.ActivityDateInserted)).map((resultRow) => (
<TableRow key={resultRow.Id}>
<TableCell><Link to={`/users/${resultRow.UserId}`} className='text-decoration-none'>{resultRow.UserName}</Link></TableCell>
<TableCell>{resultRow.RemoteEndPoint || '-'}</TableCell>
<TableCell><Link to={`/libraries/item/${resultRow.EpisodeId || resultRow.NowPlayingItemId}`} className='text-decoration-none'>{!resultRow.SeriesName ? resultRow.NowPlayingItemName : resultRow.SeriesName+' - '+ resultRow.NowPlayingItemName}</Link></TableCell>
<TableCell className='activity-client' ><span onClick={()=>openModal(resultRow)}>{resultRow.Client}</span></TableCell>
<TableCell>{Intl.DateTimeFormat('en-UK', options).format(new Date(resultRow.ActivityDateInserted))}</TableCell>
@@ -159,8 +162,14 @@ function EnhancedTableHead(props) {
{
id: 'UserName',
numeric: false,
disablePadding: true,
label: 'Last User',
disablePadding: false,
label: 'User',
},
{
id: 'RemoteEndPoint',
numeric: false,
disablePadding: false,
label: 'IP Address',
},
{
id: 'NowPlayingItemName',
@@ -172,7 +181,7 @@ function EnhancedTableHead(props) {
id: 'Client',
numeric: false,
disablePadding: false,
label: 'Last Client',
label: 'Client',
},
{
id: 'ActivityDateInserted',
@@ -190,7 +199,7 @@ function EnhancedTableHead(props) {
id: 'TotalPlays',
numeric: false,
disablePadding: false,
label: 'TotalPlays',
label: 'Total Plays',
},
];
@@ -315,7 +324,7 @@ export default function ActivityTable(props) {
{visibleRows.map((row) => (
<Row key={row.Id+row.NowPlayingItemId+row.EpisodeId} row={row} />
))}
{props.data.length===0 ? <tr><td colSpan="7" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}} className='py-2'>No Activity Found</td></tr> :''}
{props.data.length===0 ? <tr><td colSpan="8" style={{ textAlign: "center", fontStyle: "italic" ,color:"grey"}} className='py-2'>No Activity Found</td></tr> :''}
</TableBody>
</Table>

View File

@@ -16,18 +16,20 @@ function RecentlyAddedCard(props) {
<img
src={
`${"/Proxy/Items/Images/Primary?id=" +
props.data.Id +
(props.data.Type==="Episode"? props.data.SeriesId :props.data.Id) +
"&fillHeight=320&fillWidth=213&quality=50"}`
}
alt=""
onLoad={() => setLoaded(true)}
style={loaded ? { backgroundImage: `url(path/to/image.jpg)` } : { display: 'none' }}
style={loaded ? { } : { display: 'none' }}
/>
</div>
</Link>
<div className="last-item-details">
<div className="last-item-name"> {props.data.Name}</div>
<div className="last-item-name"> {(props.data.Type==="Episode"? props.data.SeriesName :props.data.Name)}</div>
</div>

View File

@@ -18,7 +18,7 @@ function LibraryCard(props) {
const MusicIcon=<FileMusicLineIcon size={"50%"} color="white"/> ;
const MixedIcon=<CheckboxMultipleBlankLineIcon size={"50%"} color="white"/> ;
const default_image=<div className="default_library_image d-flex justify-content-center align-items-center">{props.data.CollectionType==='tvshows' ? SeriesIcon : props.data.CollectionType==='movies'? MovieIcon : props.data.CollectionType==='music'? MusicIcon : MixedIcon} </div>;
const default_image=<div className="default_library_image default_library_image_hover d-flex justify-content-center align-items-center">{props.data.CollectionType==='tvshows' ? SeriesIcon : props.data.CollectionType==='movies'? MovieIcon : props.data.CollectionType==='music'? MusicIcon : MixedIcon} </div>;
function formatFileSize(sizeInBytes) {
const sizeInKB = sizeInBytes / 1024; // 1 KB = 1024 bytes
@@ -129,7 +129,7 @@ function LibraryCard(props) {
<Card.Img
variant="top"
className="library-card-banner"
className="library-card-banner library-card-banner-hover"
src={"/proxy/Items/Images/Primary?id=" + props.data.Id + "&fillWidth=800&quality=50"}
onError={() =>setImageLoaded(false)}
/>

View File

@@ -3,26 +3,17 @@ import axios from "axios";
import RecentlyAddedCard from "./RecentlyAdded/recently-added-card";
import Config from "../../../lib/config";
import "../../css/users/user-details.css";
import ErrorBoundary from "../general/ErrorBoundary";
function RecentlyAdded(props) {
const [data, setData] = useState();
const [config, setConfig] = useState();
const token = localStorage.getItem('token');
useEffect(() => {
const fetchConfig = async () => {
try {
const newConfig = await Config();
setConfig(newConfig);
} catch (error) {
console.log(error);
}
};
const fetchData = async () => {
@@ -35,38 +26,32 @@ function RecentlyAdded(props) {
const itemData = await axios.get(url, {
headers: {
Authorization: `Bearer ${config.token}`,
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
if(itemData && typeof itemData.data === 'object' && Array.isArray(itemData.data))
{
setData(itemData.data.filter((item) => ["Series", "Movie","Audio"].includes(item.Type)));
setData(itemData.data.filter((item) => ["Series", "Movie","Audio","Episode"].includes(item.Type)));
}
} catch (error) {
console.log(error);
}
};
if (!data && config) {
if (!data) {
fetchData();
}
if (!config) {
fetchConfig();
}
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
}, [data,config, props.LibraryId]);
}, [data, props.LibraryId]);
if (!data && !config) {
return <></>;
}
if (!data && config) {
if (!data) {
return <></>;
}
@@ -76,7 +61,7 @@ function RecentlyAdded(props) {
<div className="last-played-container">
{data && data.map((item) => (
<ErrorBoundary key={item.Id}>
<RecentlyAddedCard data={item} base_url={config.hostUrl} />
<RecentlyAddedCard data={item}/>
</ErrorBoundary>
))}

View File

@@ -33,10 +33,11 @@
background-size: cover;
transition: all 0.2s ease-in-out;
max-height: 170px;
height: 170px;
}
.library-card-banner:hover
.library-card-banner-hover:hover, .default_library_image_hover:hover
{
opacity: 0.5;
}
@@ -53,4 +54,24 @@
width: 100%;
height: 170px;
border-radius: 8px 8px 0px 0px;
}
transition: all 0.2s ease-in-out;
}
.form-switch .form-check-input {
border-color: var(--primary-color) !important;
}
.form-switch .form-check-input:checked {
background-color: var(--primary-color);
}
.form-switch .form-check-input:focus {
box-shadow: none !important;
border-color: var(--primary-color) !important;
}
.form-switch .form-check-input:hover {
box-shadow: none !important;
border-color: var(--primary-color) !important;
}

View File

@@ -0,0 +1,81 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import Config from "../lib/config";
import "./css/library/libraries.css";
import Loading from "./components/general/loading";
import SelectionCard from "./components/LibrarySelector/SelectionCard";
import ErrorBoundary from "./components/general/ErrorBoundary";
function LibrarySelector() {
const [data, setData] = useState();
const [config, setConfig] = useState(null);
useEffect(() => {
const fetchConfig = async () => {
try {
const newConfig = await Config();
setConfig(newConfig);
} catch (error) {
if (error.code === "ERR_NETWORK") {
console.log(error);
}
}
};
const fetchLibraries = () => {
if(config)
{
const url = `/api/TrackedLibraries`;
axios
.get(url, {
headers: {
Authorization: `Bearer ${config.token}`,
"Content-Type": "application/json",
},
})
.then((data) => {
setData(data.data);
})
.catch((error) => {
console.log(error);
});
}
};
if (!config) {
fetchConfig();
}
fetchLibraries();
const intervalId = setInterval(fetchLibraries, 60000 * 60);
return () => clearInterval(intervalId);
}, [ config]);
if (!data) {
return <Loading />;
}
console.log(data);
return (
<div className="libraries">
<h1 className="py-4">Select Libraries to Import and Track</h1>
<div xs={1} md={2} lg={4} className="g-0 libraries-container">
{data &&
data.map((item) => (
<ErrorBoundary key={item.Id} >
<SelectionCard data={item} base_url={config.hostUrl}/>
</ErrorBoundary>
))}
</div>
</div>
);
}
export default LibrarySelector;

View File

@@ -9,8 +9,7 @@ import './css/library/libraries.css';
// import LibraryOverView from './components/libraryOverview';
// import HomeStatisticCards from './components/HomeStatisticCards';
// import Sessions from './components/sessions/sessions';
import MostActiveUsers from './components/statCards/most_active_users';
import LibrarySelector from './library_selector';
@@ -52,7 +51,7 @@ function Testing() {
return (
<div className='Activity'>
<MostActiveUsers/>
<LibrarySelector/>
</div>