diff --git a/backend/classes/emby-api.js b/backend/classes/emby-api.js index 98836df..a56df89 100644 --- a/backend/classes/emby-api.js +++ b/backend/classes/emby-api.js @@ -104,8 +104,8 @@ class EmbyAPI { //Functions - async getUsers() { - if (!this.configReady) { + async getUsers(refreshConfig = false) { + if (!this.configReady || refreshConfig) { const success = await this.#fetchConfig(); if (!success) { return []; @@ -133,9 +133,9 @@ class EmbyAPI { } } - async getAdmins() { + async getAdmins(refreshConfig = false) { try { - const users = await this.getUsers(); + const users = await this.getUsers(refreshConfig); return users?.filter((user) => user.Policy.IsAdministrator) || []; } catch (error) { this.#errorHandler(error); diff --git a/backend/classes/jellyfin-api.js b/backend/classes/jellyfin-api.js index cf52917..81e244b 100644 --- a/backend/classes/jellyfin-api.js +++ b/backend/classes/jellyfin-api.js @@ -105,8 +105,8 @@ class JellyfinAPI { //Functions - async getUsers() { - if (!this.configReady) { + async getUsers(refreshConfig = false) { + if (!this.configReady || refreshConfig) { const success = await this.#fetchConfig(); if (!success) { return []; @@ -133,9 +133,9 @@ class JellyfinAPI { } } - async getAdmins() { + async getAdmins(refreshConfig = false) { try { - const users = await this.getUsers(); + const users = await this.getUsers(refreshConfig); return users?.filter((user) => user.Policy.IsAdministrator) || []; } catch (error) { this.#errorHandler(error); diff --git a/backend/classes/task-manager.js b/backend/classes/task-manager.js index 7972ea1..b06856a 100644 --- a/backend/classes/task-manager.js +++ b/backend/classes/task-manager.js @@ -45,7 +45,7 @@ class TaskManager { if (code !== 0) { console.error(`Worker ${task.name} stopped with exit code ${code}`); } - if (onExit) { + if (code !== 0 && onExit) { onExit(); } delete this.tasks[task.name]; diff --git a/backend/models/jf_library_items.js b/backend/models/jf_library_items.js index c19a1cf..16db1b1 100644 --- a/backend/models/jf_library_items.js +++ b/backend/models/jf_library_items.js @@ -50,7 +50,7 @@ const jf_library_items_mapping = (item) => ({ ? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]] : null, archived: false, - Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(filterInvalidGenres(item.Genres.map(titleCase))) : [], + Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(item.Genres.map(titleCase)) : [], }); // Utility function to title-case a string @@ -62,53 +62,6 @@ function titleCase(str) { .join(" "); } -function filterInvalidGenres(genres) { - const validGenres = [ - "Action", - "Adventure", - "Animated", - "Biography", - "Comedy", - "Crime", - "Dance", - "Disaster", - "Documentary", - "Drama", - "Erotic", - "Family", - "Fantasy", - "Found Footage", - "Historical", - "Horror", - "Independent", - "Legal", - "Live Action", - "Martial Arts", - "Musical", - "Mystery", - "Noir", - "Performance", - "Political", - "Romance", - "Satire", - "Science Fiction", - "Short", - "Silent", - "Slasher", - "Sports", - "Spy", - "Superhero", - "Supernatural", - "Suspense", - "Teen", - "Thriller", - "War", - "Western", - ]; - - return genres.filter((genre) => validGenres.map((g) => g.toLowerCase()).includes(genre.toLowerCase())); -} - module.exports = { jf_library_items_columns, jf_library_items_mapping, diff --git a/backend/routes/api.js b/backend/routes/api.js index 4c1862b..7cbe0f5 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -463,7 +463,24 @@ router.post("/setconfig", async (req, res) => { settings.ServerID = systemInfo?.Id || null; - let query = 'UPDATE app_config SET settings=$1 where "ID"=1'; + const query = 'UPDATE app_config SET settings=$1 where "ID"=1'; + + await db.query(query, [settings]); + } + } + + const admins = await API.getAdmins(true); + const preferredAdmin = await new configClass().getPreferedAdmin(); + if (admins && admins.length > 0 && preferredAdmin && !admins.map((item) => item.Id).includes(preferredAdmin)) { + const newAdmin = admins[0]; + 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 || {}; + + settings.preferred_admin = { userid: newAdmin.Id, username: newAdmin.Name }; + + const query = 'UPDATE app_config SET settings=$1 where "ID"=1'; await db.query(query, [settings]); } diff --git a/backend/routes/auth.js b/backend/routes/auth.js index f6fab73..6afef08 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -20,16 +20,23 @@ router.post("/login", async (req, res) => { try { const { username, password } = req.body; - if (!username || !password || password === CryptoJS.SHA3("").toString()) { + const query = "SELECT * FROM app_config"; + const { rows: login } = await db.query(query); + + if ( + (!username || !password || password === CryptoJS.SHA3("").toString()) && + login.length > 0 && + login[0].REQUIRE_LOGIN == true + ) { res.sendStatus(401); return; } - const query = 'SELECT * FROM app_config WHERE ("APP_USER" = $1 AND "APP_PASSWORD" = $2) OR "REQUIRE_LOGIN" = false'; - const values = [username, password]; - const { rows: login } = await db.query(query, values); + const loginUser = login.filter( + (user) => (user.APP_USER === username && user.APP_PASSWORD === password) || user.REQUIRE_LOGIN == false + ); - if (login.length > 0 || (username === JS_USER && password === CryptoJS.SHA3(JS_PASSWORD).toString())) { + if (loginUser.length > 0 || (username === JS_USER && password === CryptoJS.SHA3(JS_PASSWORD).toString())) { const user = { id: 1, username: username }; jwt.sign({ user }, JWT_SECRET, (err, token) => { diff --git a/backend/routes/proxy.js b/backend/routes/proxy.js index 18fdf1f..61a4c28 100644 --- a/backend/routes/proxy.js +++ b/backend/routes/proxy.js @@ -148,7 +148,7 @@ router.get("/getSessions", async (req, res) => { router.get("/getAdminUsers", async (req, res) => { try { - const adminUser = await API.getAdmins(); + const adminUser = await API.getAdmins(true); res.send(adminUser); } catch (error) { res.status(503); diff --git a/backend/routes/stats.js b/backend/routes/stats.js index 2a78f66..a727991 100644 --- a/backend/routes/stats.js +++ b/backend/routes/stats.js @@ -407,9 +407,9 @@ router.post("/getLibraryLastPlayed", async (req, res) => { } }); -router.post("/getViewsOverTime", async (req, res) => { +router.get("/getViewsOverTime", async (req, res) => { try { - const { days } = req.body; + const { days } = req.query; let _days = days; if (days === undefined) { _days = 30; @@ -446,9 +446,9 @@ router.post("/getViewsOverTime", async (req, res) => { } }); -router.post("/getViewsByDays", async (req, res) => { +router.get("/getViewsByDays", async (req, res) => { try { - const { days } = req.body; + const { days } = req.query; let _days = days; if (days === undefined) { _days = 30; @@ -481,9 +481,9 @@ router.post("/getViewsByDays", async (req, res) => { } }); -router.post("/getViewsByHour", async (req, res) => { +router.get("/getViewsByHour", async (req, res) => { try { - const { days } = req.body; + const { days } = req.query; let _days = days; if (days === undefined) { _days = 30; @@ -516,6 +516,41 @@ router.post("/getViewsByHour", async (req, res) => { } }); +router.get("/getViewsByLibraryType", async (req, res) => { + try { + const { days = 30 } = req.query; + + const { rows } = await db.query(` + SELECT COALESCE(i."Type", 'Other') AS type, COUNT(a."NowPlayingItemId") AS count + FROM jf_playback_activity a LEFT JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId" + WHERE a."ActivityDateInserted" BETWEEN NOW() - CAST($1 || ' days' as INTERVAL) AND NOW() + GROUP BY i."Type" + `, [days]); + + const supportedTypes = new Set(["Audio", "Movie", "Series", "Other"]); + /** @type {Map} */ + const reorganizedData = new Map(); + + rows.forEach((item) => { + const { type, count } = item; + + if (!supportedTypes.has(type)) return; + reorganizedData.set(type, count); + }); + + supportedTypes.forEach((type) => { + if (reorganizedData.has(type)) return; + reorganizedData.set(type, 0); + }); + + res.send(Object.fromEntries(reorganizedData)); + } catch (error) { + console.log(error); + res.status(503); + res.send(error); + } +}); + router.get("/getGenreUserStats", async (req, res) => { try { const { size = 50, page = 1, userid } = req.query; diff --git a/backend/routes/sync.js b/backend/routes/sync.js index 5247c9f..4f811ce 100644 --- a/backend/routes/sync.js +++ b/backend/routes/sync.js @@ -395,12 +395,13 @@ async function removeOrphanedData() { syncTask.loggedData.push({ color: "yellow", Message: "Removing Orphaned FileInfo/Episode/Season Records" }); await db.query("CALL jd_remove_orphaned_data()"); - const archived_items = await db - .query(`select "Id" from jf_library_items where archived=true and "Type"='Series'`) - .then((res) => res.rows.map((row) => row.Id)); - const archived_seasons = await db - .query(`select "Id" from jf_library_seasons where archived=true`) - .then((res) => res.rows.map((row) => row.Id)); + const archived_items_query = `select i."Id" from jf_library_items i join jf_library_seasons s on s."SeriesId"=i."Id" and s.archived=false where i.archived=true and i."Type"='Series' + union + select i."Id" from jf_library_items i join jf_library_episodes e on e."SeriesId"=i."Id" and e.archived=false where i.archived=true and i."Type"='Series' + `; + const archived_items = await db.query(archived_items_query).then((res) => res.rows.map((row) => row.Id)); + const archived_seasons_query = `select s."Id" from jf_library_seasons s join jf_library_episodes e on e."SeasonId"=s."Id" and e.archived=false where s.archived=true`; + const archived_seasons = await db.query(archived_seasons_query).then((res) => res.rows.map((row) => row.Id)); if (!(await _sync.updateSingleFieldOnDB("jf_library_seasons", archived_items, "archived", true, "SeriesId"))) { syncTask.loggedData.push({ color: "red", Message: "Error archiving library seasons" }); await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED); diff --git a/backend/swagger.json b/backend/swagger.json index 84ec7fe..00fc18b 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -3504,7 +3504,7 @@ } }, "/stats/getViewsOverTime": { - "post": { + "get": { "tags": [ "Stats" ], @@ -3526,16 +3526,9 @@ "type": "string" }, { - "name": "body", - "in": "body", - "schema": { - "type": "object", - "properties": { - "days": { - "example": "any" - } - } - } + "name": "days", + "in": "query", + "type": "string" } ], "responses": { @@ -3558,7 +3551,7 @@ } }, "/stats/getViewsByDays": { - "post": { + "get": { "tags": [ "Stats" ], @@ -3580,16 +3573,9 @@ "type": "string" }, { - "name": "body", - "in": "body", - "schema": { - "type": "object", - "properties": { - "days": { - "example": "any" - } - } - } + "name": "days", + "in": "query", + "type": "string" } ], "responses": { @@ -3612,7 +3598,7 @@ } }, "/stats/getViewsByHour": { - "post": { + "get": { "tags": [ "Stats" ], @@ -3634,16 +3620,56 @@ "type": "string" }, { - "name": "body", - "in": "body", - "schema": { - "type": "object", - "properties": { - "days": { - "example": "any" - } - } - } + "name": "days", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + }, + "503": { + "description": "Service Unavailable" + } + } + } + }, + "/stats/getViewsByLibraryType": { + "get": { + "tags": [ + "Stats" + ], + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "x-api-token", + "in": "header", + "type": "string" + }, + { + "name": "req", + "in": "query", + "type": "string" + }, + { + "name": "days", + "in": "query", + "type": "string" } ], "responses": { diff --git a/backend/tasks/BackupTask.js b/backend/tasks/BackupTask.js index 352a5fa..7f55e97 100644 --- a/backend/tasks/BackupTask.js +++ b/backend/tasks/BackupTask.js @@ -42,8 +42,9 @@ async function runBackupTask(triggerType = triggertype.Automatic) { } } -parentPort.on("message", (message) => { +parentPort.on("message", async (message) => { if (message.command === "start") { - runBackupTask(message.triggertype); + await runBackupTask(message.triggertype); + process.exit(0); // Exit the worker after the task is done } }); diff --git a/backend/tasks/FullSyncTask.js b/backend/tasks/FullSyncTask.js index 20ca170..196bc05 100644 --- a/backend/tasks/FullSyncTask.js +++ b/backend/tasks/FullSyncTask.js @@ -28,8 +28,9 @@ async function runFullSyncTask(triggerType = triggertype.Automatic) { } } -parentPort.on("message", (message) => { +parentPort.on("message", async (message) => { if (message.command === "start") { - runFullSyncTask(message.triggertype); + await runFullSyncTask(message.triggertype); + process.exit(0); // Exit the worker after the task is done } }); diff --git a/backend/tasks/PlaybackReportingPluginSyncTask.js b/backend/tasks/PlaybackReportingPluginSyncTask.js index 73c5911..63292e9 100644 --- a/backend/tasks/PlaybackReportingPluginSyncTask.js +++ b/backend/tasks/PlaybackReportingPluginSyncTask.js @@ -27,8 +27,9 @@ async function runPlaybackReportingPluginSyncTask() { } } -parentPort.on("message", (message) => { +parentPort.on("message", async (message) => { if (message.command === "start") { - runPlaybackReportingPluginSyncTask(); + await runPlaybackReportingPluginSyncTask(); + process.exit(0); // Exit the worker after the task is done } }); diff --git a/backend/tasks/RecentlyAddedItemsSyncTask.js b/backend/tasks/RecentlyAddedItemsSyncTask.js index a40dc13..85f0676 100644 --- a/backend/tasks/RecentlyAddedItemsSyncTask.js +++ b/backend/tasks/RecentlyAddedItemsSyncTask.js @@ -28,8 +28,9 @@ async function runPartialSyncTask(triggerType = triggertype.Automatic) { } } -parentPort.on("message", (message) => { +parentPort.on("message", async (message) => { if (message.command === "start") { - runPartialSyncTask(message.triggertype); + await runPartialSyncTask(message.triggertype); + process.exit(0); // Exit the worker after the task is done } }); diff --git a/package-lock.json b/package-lock.json index d39c07f..c04afa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jfstat", - "version": "1.1.5", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 707978c..eb28e84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jfstat", - "version": "1.1.5", + "version": "1.1.6", "private": true, "main": "src/index.jsx", "scripts": { diff --git a/src/pages/components/library/library-items.jsx b/src/pages/components/library/library-items.jsx index 04e420b..3a45cce 100644 --- a/src/pages/components/library/library-items.jsx +++ b/src/pages/components/library/library-items.jsx @@ -23,8 +23,6 @@ function LibraryItems(props) { localStorage.getItem("PREF_sortAsc") != undefined ? localStorage.getItem("PREF_sortAsc") == "true" : true ); - console.log(sortOrder); - const archive = { all: "all", archived: "true", @@ -212,7 +210,11 @@ function LibraryItems(props) { } }) .map((item) => ( - + ))} diff --git a/src/pages/components/settings/settingsConfig.jsx b/src/pages/components/settings/settingsConfig.jsx index fe6dce5..6fe7a71 100644 --- a/src/pages/components/settings/settingsConfig.jsx +++ b/src/pages/components/settings/settingsConfig.jsx @@ -44,6 +44,20 @@ export default function SettingsConfig() { set12hr(Boolean(storage_12hr)); } + const fetchAdmins = async () => { + try { + const adminData = await axios.get(`/proxy/getAdminUsers`, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + setAdmins(adminData.data); + } catch (error) { + console.log(error); + } + }; + useEffect(() => { Config.getConfig() .then((config) => { @@ -59,20 +73,6 @@ export default function SettingsConfig() { setsubmissionMessage("Error Retrieving Configuration. Unable to contact Backend Server"); }); - const fetchAdmins = async () => { - try { - const adminData = await axios.get(`/proxy/getAdminUsers`, { - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - }); - setAdmins(adminData.data); - } catch (error) { - console.log(error); - } - }; - fetchAdmins(); }, [token]); @@ -91,6 +91,8 @@ export default function SettingsConfig() { console.log("Config updated successfully:", response.data); setisSubmitted("Success"); setsubmissionMessage("Successfully updated configuration"); + Config.setConfig(); + fetchAdmins(); }) .catch((error) => { let errorMessage = error.response.data.errorMessage; @@ -98,7 +100,6 @@ export default function SettingsConfig() { setisSubmitted("Failed"); setsubmissionMessage(`Error Updating Configuration: ${errorMessage}`); }); - Config.setConfig(); } async function handleFormSubmitExternal(event) { @@ -233,9 +234,13 @@ export default function SettingsConfig() { {isSubmitted !== "" ? ( isSubmitted === "Failed" ? ( - {submissionMessage} + + {submissionMessage} + ) : ( - {submissionMessage} + + {submissionMessage} + ) ) : ( <> @@ -265,9 +270,13 @@ export default function SettingsConfig() { {isSubmittedExternal !== "" ? ( isSubmittedExternal === "Failed" ? ( - {submissionMessageExternal} + + {submissionMessageExternal} + ) : ( - {submissionMessageExternal} + + {submissionMessageExternal} + ) ) : ( <> diff --git a/src/pages/components/statCards/genre-stat-card.jsx b/src/pages/components/statCards/genre-stat-card.jsx index c1d96a4..df213e2 100644 --- a/src/pages/components/statCards/genre-stat-card.jsx +++ b/src/pages/components/statCards/genre-stat-card.jsx @@ -10,12 +10,26 @@ import i18next from "i18next"; function GenreStatCard(props) { const [maxRange, setMaxRange] = useState(100); + const [data, setData] = useState(props.data); useEffect(() => { const maxDuration = props.data.reduce((max, item) => { return Math.max(max, parseFloat((props.dataKey == "duration" ? item.duration : item.plays) || 0)); }, 0); setMaxRange(maxDuration); + + let sorted = [...props.data] + .sort((a, b) => { + const valueA = parseFloat(props.dataKey === "duration" ? a.duration : a.plays) || 0; + const valueB = parseFloat(props.dataKey === "duration" ? b.duration : b.plays) || 0; + return valueB - valueA; // Descending order + }) + .slice(0, 15); // Take only the top 10 + + // Sort top 10 genres alphabetically + sorted = sorted.sort((a, b) => a.genre.localeCompare(b.genre)); + + setData(sorted); }, [props.data, props.dataKey]); const CustomTooltip = ({ active, payload }) => { @@ -67,7 +81,7 @@ function GenreStatCard(props) { - + diff --git a/src/pages/components/statistics/daily-play-count.jsx b/src/pages/components/statistics/daily-play-count.jsx index 3e896fc..e7b9316 100644 --- a/src/pages/components/statistics/daily-play-count.jsx +++ b/src/pages/components/statistics/daily-play-count.jsx @@ -17,12 +17,11 @@ function DailyPlayStats(props) { useEffect(() => { const fetchLibraries = () => { - const url = `/stats/getViewsOverTime`; + const url = `/stats/getViewsOverTime?days=${props.days}`; axios - .post( + .get( url, - { days: props.days }, { headers: { Authorization: `Bearer ${token}`, diff --git a/src/pages/components/statistics/play-stats-by-day.jsx b/src/pages/components/statistics/play-stats-by-day.jsx index d04843c..c7f5c09 100644 --- a/src/pages/components/statistics/play-stats-by-day.jsx +++ b/src/pages/components/statistics/play-stats-by-day.jsx @@ -13,12 +13,11 @@ function PlayStatsByDay(props) { useEffect(() => { const fetchLibraries = () => { - const url = `/stats/getViewsByDays`; + const url = `/stats/getViewsByDays?days=${props.days}`; axios - .post( + .get( url, - { days: props.days }, { headers: { Authorization: `Bearer ${token}`, diff --git a/src/pages/components/statistics/play-stats-by-hour.jsx b/src/pages/components/statistics/play-stats-by-hour.jsx index 4403400..f1895d3 100644 --- a/src/pages/components/statistics/play-stats-by-hour.jsx +++ b/src/pages/components/statistics/play-stats-by-hour.jsx @@ -12,12 +12,11 @@ function PlayStatsByHour(props) { useEffect(() => { const fetchLibraries = () => { - const url = `/stats/getViewsByHour`; + const url = `/stats/getViewsByHour?days=${props.days}`; axios - .post( + .get( url, - { days: props.days }, { headers: { Authorization: `Bearer ${token}`,