mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
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:
@@ -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");
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
`);
|
||||
};
|
||||
|
||||
@@ -144,6 +144,7 @@ router.get('/Items/Images/Backdrop/', async(req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -370,6 +370,8 @@ router.get("/getRecentlyAdded", async (req, res) => {
|
||||
{
|
||||
url+=`?parentId=${libraryid}`;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const response_data = await axios_instance.get(url, {
|
||||
headers: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
99
src/pages/components/LibrarySelector/SelectionCard.js
Normal file
99
src/pages/components/LibrarySelector/SelectionCard.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
81
src/pages/library_selector.js
Normal file
81
src/pages/library_selector.js
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user