diff --git a/backend/classes/api-loader.js b/backend/classes/api-loader.js new file mode 100644 index 0000000..ef7135b --- /dev/null +++ b/backend/classes/api-loader.js @@ -0,0 +1,13 @@ +const JellyfinAPI = require("./jellyfin-api"); +const EmbyAPI = require("./emby-api"); + +function API() { + const USE_EMBY_API = (process.env.IS_EMBY_API || "false").toLowerCase() === "true"; + if (USE_EMBY_API) { + return new EmbyAPI(); + } else { + return new JellyfinAPI(); + } +} + +module.exports = API(); diff --git a/backend/classes/config.js b/backend/classes/config.js index ff5b4fe..47812de 100644 --- a/backend/classes/config.js +++ b/backend/classes/config.js @@ -20,6 +20,7 @@ class Config { settings: config[0].settings, api_keys: config[0].api_keys, state: state, + IS_JELLYFIN: (process.env.IS_EMBY_API || "false").toLowerCase() === "false", }; } catch (error) { return { error: "Config Details Not Found" }; diff --git a/backend/classes/emby-api.js b/backend/classes/emby-api.js new file mode 100644 index 0000000..c28ee03 --- /dev/null +++ b/backend/classes/emby-api.js @@ -0,0 +1,503 @@ +const configClass = require("./config"); +const { axios } = require("./axios"); + +class EmbyAPI { + constructor() { + this.config = null; + this.configReady = false; + this.#checkReadyStatus(); + } + //Helper classes + #checkReadyStatus() { + let checkConfigError = setInterval(async () => { + const _config = await new configClass().getConfig(); + if (!_config.error && _config.state === 2) { + clearInterval(checkConfigError); + this.config = _config; + this.configReady = true; + } + }, 5000); // Check every 5 seconds + } + + #errorHandler(error, url) { + if (error.response) { + console.log("[EMBY-API]: " + this.#httpErrorMessageHandler(error)); + } else { + console.log("[EMBY-API]", { + ErrorAt: this.#getErrorLineNumber(error), + ErrorLines: this.#getErrorLineNumbers(error), + Message: error.message, + url: url, + // StackTrace: this.#getStackTrace(error), + }); + } + } + + #httpErrorMessageHandler(error) { + let message = ""; + switch (error.response.status) { + case 400: + message = "400 Bad Request"; + break; + case 401: + message = "401 Unauthorized"; + break; + case 403: + message = "403 Access Forbidden"; + break; + case 404: + message = `404 URL Not Found : ${error.request.path}`; + break; + case 503: + message = `503 Service Unavailable : ${error.request.path}`; + break; + default: + message = `Unexpected status code: ${error.response.status}`; + } + return message; + } + + #getErrorLineNumber(error) { + const stackTrace = this.#getStackTrace(error); + const errorLine = stackTrace[1].trim(); + const lineNumber = errorLine.substring(errorLine.lastIndexOf("\\") + 1, errorLine.lastIndexOf(")")); + return lineNumber; + } + + #getErrorLineNumbers(error) { + const stackTrace = this.#getStackTrace(error); + let errorLines = []; + + for (const [index, line] of stackTrace.entries()) { + if (line.trim().startsWith("at")) { + const errorLine = line.trim(); + const startSubstring = errorLine.lastIndexOf("\\") == -1 ? errorLine.indexOf("(") + 1 : errorLine.lastIndexOf("\\") + 1; + const endSubstring = errorLine.lastIndexOf(")") == -1 ? errorLine.length : errorLine.lastIndexOf(")"); + const lineNumber = errorLine.substring(startSubstring, endSubstring); + errorLines.push({ TraceIndex: index, line: lineNumber }); + } + } + + return errorLines; + } + + #getStackTrace(error) { + const stackTrace = error.stack.split("\n"); + return stackTrace; + } + + #delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + //Functions + + async getUsers() { + if (!this.configReady) { + return []; + } + try { + const url = `${this.config.JF_HOST}/Users`; + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + return response?.data || []; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getAdmins() { + try { + const users = await this.getUsers(); + return users?.filter((user) => user.Policy.IsAdministrator) || []; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getItemsByID({ ids, params }) { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/Items?ids=${ids}`; + let startIndex = params && params.startIndex ? params.startIndex : 0; + let increment = params && params.increment ? params.increment : 200; + let recursive = params && params.recursive !== undefined ? params.recursive : true; + let total = 200; + + let final_response = []; + while (startIndex < total || total === undefined) { + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + params: { + fields: "MediaSources,DateCreated", + startIndex: startIndex, + recursive: recursive, + limit: increment, + isMissing: false, + excludeLocationTypes: "Virtual", + }, + }); + + total = response?.data?.TotalRecordCount ?? 0; + startIndex += increment; + + const result = response?.data?.Items || []; + + final_response.push(...result); + if (response.data.TotalRecordCount === undefined) { + break; + } + + await this.#delay(10); + } + + return final_response; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getItemsFromParentId({ id, itemid, params, ws, syncTask, wsMessage }) { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/Items?ParentId=${id}`; + if (itemid && itemid != null) { + url += `&Ids=${itemid}`; + } + + let startIndex = params && params.startIndex ? params.startIndex : 0; + let increment = params && params.increment ? params.increment : 200; + let recursive = params && params.recursive !== undefined ? params.recursive : true; + let total = 200; + + let AllItems = []; + while (startIndex < total || total === undefined) { + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + params: { + fields: "MediaSources,DateCreated", + startIndex: startIndex, + recursive: recursive, + limit: increment, + isMissing: false, + excludeLocationTypes: "Virtual", + }, + }); + + total = response?.data?.TotalRecordCount || 0; + startIndex += increment; + + const result = response?.data?.Items || []; + + AllItems.push(...result); + + if (response.data.TotalRecordCount === undefined) { + break; + } + if (ws && syncTask && wsMessage) { + ws(syncTask.wsKey, { type: "Update", message: `${wsMessage} - ${((startIndex / total) * 100).toFixed(2)}%` }); + } + + await this.#delay(10); + } + + return AllItems; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getItemInfo({ itemID, userid }) { + if (!this.configReady) { + return []; + } + try { + if (!userid || userid == null) { + await new configClass().getPreferedAdmin().then(async (adminid) => { + if (!adminid || adminid == null) { + userid = (await this.getAdmins())[0].Id; + } else { + userid = adminid; + } + }); + } + + let url = `${this.config.JF_HOST}/Items/${itemID}/playbackinfo?userId=${userid}`; + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + return response?.data?.MediaSources || 0; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getLibraries() { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/Library/MediaFolders`; + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + const libraries = + response?.data?.Items?.filter((library) => !["boxsets", "playlists"].includes(library.CollectionType)) || []; + + return libraries; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getSeasons(SeriesId) { + if (!this.configReady) { + return []; + } + let url = `${this.config.JF_HOST}/Shows/${SeriesId}/Seasons`; + try { + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + return response?.data?.Items?.filter((item) => item.LocationType !== "Virtual") || []; + } catch (error) { + this.#errorHandler(error, url); + return []; + } + } + + async getEpisodes({ SeriesId, SeasonId }) { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/Shows/${SeriesId}/Episodes?seasonId=${SeasonId}`; + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + return response?.data?.Items?.filter((item) => item.LocationType !== "Virtual") || []; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getRecentlyAdded({ libraryid, limit = 20, userid }) { + if (!this.configReady) { + return []; + } + try { + if (!userid || userid == null) { + let adminid = await new configClass().getPreferedAdmin(); + if (!adminid || adminid == null) { + userid = (await this.getAdmins())[0].Id; + } else { + userid = adminid; + } + } + + let url = `${this.config.JF_HOST}/Users/${userid}/Items/Latest?Limit=${limit}`; + + if (libraryid && libraryid != null) { + url += `&ParentId=${libraryid}`; + } + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + params: { + fields: "MediaSources,DateCreated", + }, + }); + + const items = response?.data?.filter((item) => item.LocationType !== "Virtual") || []; + + return items; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getSessions() { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/sessions`; + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + let result = response.data && Array.isArray(response.data) ? response.data : []; + + if (result.length > 0) { + result = result.filter( + (session) => + session.NowPlayingItem !== undefined && + session.NowPlayingItem.Type != "Trailer" && + session.NowPlayingItem.ProviderIds["prerolls.video"] == undefined + ); + } + return result; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async getInstalledPlugins() { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/plugins`; + + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + return response.data; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + async StatsSubmitCustomQuery(query) { + if (!this.configReady) { + return []; + } + try { + let url = `${this.config.JF_HOST}/user_usage_stats/submit_custom_query`; + + const response = await axios.post( + url, + { + CustomQueryString: query, + }, + { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + } + ); + return response.data.results; + } catch (error) { + this.#errorHandler(error); + return []; + } + } + + #isValidUrl(string) { + try { + new URL(string); + return true; + } catch (err) { + return false; + } + } + + async validateSettings(url, apikey) { + let result = { isValid: false, status: 400, errorMessage: "Invalid URL", url: url, cleanedUrl: "" }; + try { + let _url = url.replace(/\/web\/index\.html#!\/home\.html$/, ""); + + _url = _url.replace(/\/$/, ""); + if (!/^https?:\/\//i.test(_url)) { + _url = "http://" + _url; + } + + if (!url.includes("/emby")) { + _url = _url + "/emby"; + } + + result.cleanedUrl = _url; + + console.log(_url, this.#isValidUrl(_url)); + if (!this.#isValidUrl(_url)) { + return result; + } + + const validation_url = _url.replace(/\/$/, "") + "/system/configuration"; + + const response = await axios.get(validation_url, { + headers: { + "X-MediaBrowser-Token": apikey, + }, + }); + result.isValid = response.status == 200; + return result; + } catch (error) { + this.#errorHandler(error); + result.isValid = false; + result.status = error?.response?.status ?? 400; + result.errorMessage = + error?.response != null + ? this.#httpErrorMessageHandler(error) + : error.code == "ENOTFOUND" + ? "Unable to connect. Please check the URL and your network connection." + : error.message; + return result; + } + } + + async systemInfo() { + if (!this.configReady) { + return []; + } + let url = `${this.config.JF_HOST}/system/info`; + try { + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + return response?.data || {}; + } catch (error) { + this.#errorHandler(error, url); + return {}; + } + } +} + +module.exports = EmbyAPI; diff --git a/backend/classes/jellyfin-api.js b/backend/classes/jellyfin-api.js index 92f76c0..6a8aaed 100644 --- a/backend/classes/jellyfin-api.js +++ b/backend/classes/jellyfin-api.js @@ -475,6 +475,25 @@ class JellyfinAPI { return result; } } + + async systemInfo() { + if (!this.configReady) { + return []; + } + let url = `${this.config.JF_HOST}/system/info`; + try { + const response = await axios.get(url, { + headers: { + "X-MediaBrowser-Token": this.config.JF_API_KEY, + }, + }); + + return response?.data || {}; + } catch (error) { + this.#errorHandler(error, url); + return {}; + } + } } module.exports = JellyfinAPI; diff --git a/backend/routes/api.js b/backend/routes/api.js index c24e364..4c10884 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -8,12 +8,11 @@ const { randomUUID } = require("crypto"); const { axios } = require("../classes/axios"); const configClass = require("../classes/config"); const { checkForUpdates } = require("../version-control"); -const JellyfinAPI = require("../classes/jellyfin-api"); +const API = require("../classes/api-loader"); const { sendUpdate } = require("../ws"); const moment = require("moment"); const router = express.Router(); -const Jellyfin = new JellyfinAPI(); //Functions function groupActivity(rows) { @@ -47,6 +46,29 @@ function groupActivity(rows) { return groupedResults; } +function groupRecentlyAdded(rows) { + const groupedResults = {}; + rows.forEach((row) => { + if (row.Type != "Movie") { + const key = row.SeriesId + row.SeasonId; + if (groupedResults[key]) { + groupedResults[key].NewEpisodeCount++; + } else { + groupedResults[key] = { ...row }; + if (row.Type != "Series" && row.Type != "Movie") { + groupedResults[key].NewEpisodeCount = 1; + } + } + } else { + groupedResults[row.Id] = { + ...row, + }; + } + }); + + return Object.values(groupedResults); +} + async function purgeLibraryItems(id, withActivity, purgeAll = false) { let items_query = `select * from jf_library_items where "ParentId"=$1`; @@ -118,6 +140,7 @@ router.get("/getconfig", async (req, res) => { APP_USER: config.APP_USER, settings: config.settings, REQUIRE_LOGIN: config.REQUIRE_LOGIN, + IS_JELLYFIN: config.IS_JELLYFIN, }; res.send(payload); @@ -128,9 +151,9 @@ router.get("/getconfig", async (req, res) => { router.get("/getRecentlyAdded", async (req, res) => { try { - const { libraryid, limit = 10 } = req.query; + const { libraryid, limit = 50, GroupResults = true } = req.query; - let recentlyAddedFronJellystat = await Jellyfin.getRecentlyAdded({ libraryid: libraryid }); + let recentlyAddedFronJellystat = await API.getRecentlyAdded({ libraryid: libraryid }); let recentlyAddedFronJellystatMapped = recentlyAddedFronJellystat.map((item) => { return { @@ -206,7 +229,14 @@ router.get("/getRecentlyAdded", async (req, res) => { ); } - res.send([...recentlyAddedFronJellystatMapped, ...rows]); + let recentlyAdded = [...recentlyAddedFronJellystatMapped, ...rows]; + recentlyAdded = recentlyAdded.filter((item) => item.Type !== "Series"); + + if (GroupResults == true) { + recentlyAdded = groupRecentlyAdded(recentlyAdded); + } + + res.send(recentlyAdded); return; } catch (error) { res.status(503); @@ -226,7 +256,7 @@ router.post("/setconfig", async (req, res) => { var url = JF_HOST; - const validation = await Jellyfin.validateSettings(url, JF_API_KEY); + const validation = await API.validateSettings(url, JF_API_KEY); if (validation.isValid === false) { res.status(validation.status); res.send(validation); @@ -241,6 +271,22 @@ router.post("/setconfig", async (req, res) => { } const { rows } = await db.query(query, [validation.cleanedUrl, JF_API_KEY]); + + const systemInfo = await API.systemInfo(); + + if (systemInfo && systemInfo != {}) { + 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.ServerID = systemInfo?.Id || null; + + let query = 'UPDATE app_config SET settings=$1 where "ID"=1'; + + await db.query(query, [settings]); + } + } res.send(rows); } catch (error) { console.log(error); @@ -400,7 +446,7 @@ router.get("/TrackedLibraries", async (req, res) => { } try { - const libraries = await Jellyfin.getLibraries(); + const libraries = await API.getLibraries(); const ExcludedLibraries = config.settings?.ExcludedLibraries || []; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index b31f329..82df460 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -4,8 +4,7 @@ const db = require("../db"); const jwt = require("jsonwebtoken"); const configClass = require("../classes/config"); const packageJson = require("../../package.json"); -const JellyfinAPI = require("../classes/jellyfin-api"); -const Jellyfin = new JellyfinAPI(); +const API = require("../classes/api-loader"); const JWT_SECRET = process.env.JWT_SECRET; const JS_USER = process.env.JS_USER; @@ -98,7 +97,7 @@ router.post("/configSetup", async (req, res) => { var url = JF_HOST; - const validation = await Jellyfin.validateSettings(url, JF_API_KEY); + const validation = await API.validateSettings(url, JF_API_KEY); if (validation.isValid === false) { res.status(validation.status); res.send(validation); @@ -114,6 +113,22 @@ router.post("/configSetup", async (req, res) => { } const { rows } = await db.query(query, [validation.cleanedUrl, JF_API_KEY]); + + const systemInfo = await API.systemInfo(); + + if (systemInfo && systemInfo != {}) { + 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.Tasks = systemInfo?.Id || null; + + let query = 'UPDATE app_config SET settings=$1 where "ID"=1'; + + await db.query(query, [settings]); + } + } res.send(rows); } else { res.sendStatus(500); diff --git a/backend/routes/proxy.js b/backend/routes/proxy.js index 8cf756e..5fe4ca2 100644 --- a/backend/routes/proxy.js +++ b/backend/routes/proxy.js @@ -2,9 +2,8 @@ const express = require("express"); const { axios } = require("../classes/axios"); const configClass = require("../classes/config"); -const JellyfinAPI = require("../classes/jellyfin-api"); +const API = require("../classes/api-loader"); -const Jellyfin = new JellyfinAPI(); const router = express.Router(); router.get("/web/assets/img/devices/", async (req, res) => { @@ -133,7 +132,7 @@ router.get("/Users/Images/Primary/", async (req, res) => { router.get("/getSessions", async (req, res) => { try { - const sessions = await Jellyfin.getSessions(); + const sessions = await API.getSessions(); res.send(sessions); } catch (error) { res.status(503); @@ -143,7 +142,7 @@ router.get("/getSessions", async (req, res) => { router.get("/getAdminUsers", async (req, res) => { try { - const adminUser = await Jellyfin.getAdmins(); + const adminUser = await API.getAdmins(); res.send(adminUser); } catch (error) { res.status(503); @@ -155,7 +154,7 @@ router.get("/getRecentlyAdded", async (req, res) => { try { const { libraryid } = req.query; - const recentlyAdded = await Jellyfin.getRecentlyAdded({ libraryid: libraryid }); + const recentlyAdded = await API.getRecentlyAdded({ libraryid: libraryid }); res.send(recentlyAdded); } catch (error) { res.status(503); @@ -163,7 +162,7 @@ router.get("/getRecentlyAdded", async (req, res) => { } }); -//Jellyfin related functions +//API related functions router.post("/validateSettings", async (req, res) => { const { url, apikey } = req.body; @@ -174,7 +173,7 @@ router.post("/validateSettings", async (req, res) => { return; } - const validation = await Jellyfin.validateSettings(url, apikey); + const validation = await API.validateSettings(url, apikey); if (validation.isValid === false) { res.status(validation.status); res.send(validation.errorMessage); diff --git a/backend/routes/sync.js b/backend/routes/sync.js index 46ef089..1603c48 100644 --- a/backend/routes/sync.js +++ b/backend/routes/sync.js @@ -12,8 +12,8 @@ const taskName = require("../logging/taskName"); const triggertype = require("../logging/triggertype"); const configClass = require("../classes/config"); -const JellyfinAPI = require("../classes/jellyfin-api"); -const Jellyfin = new JellyfinAPI(); +const API = require("../classes/api-loader"); + const router = express.Router(); @@ -86,7 +86,7 @@ async function syncUserData() { const _sync = new sync(); - const data = await Jellyfin.getUsers(); + const data = await API.getUsers(); const existingIds = await _sync.getExistingIDsforTable("jf_users"); // get existing user Ids from the db @@ -494,10 +494,10 @@ async function syncPlaybackPluginData() { PlaybacksyncTask.loggedData.push({ color: "lawngreen", Message: "Syncing..." }); //Playback Reporting Plugin Check - const installed_plugins = await Jellyfin.getInstalledPlugins(); + const installed_plugins = await API.getInstalledPlugins(); const hasPlaybackReportingPlugin = installed_plugins.filter( - (plugins) => plugins?.ConfigurationFileName === "Jellyfin.Plugin.PlaybackReporting.xml" + (plugins) => plugins?.ConfigurationFileName === "Jellyfin.Plugin.PlaybackReporting.xml"//TO-DO Change this to the correct plugin name ); if (!hasPlaybackReportingPlugin || hasPlaybackReportingPlugin.length === 0) { @@ -538,7 +538,7 @@ async function syncPlaybackPluginData() { PlaybacksyncTask.loggedData.push({ color: "dodgerblue", Message: "Query built. Executing." }); // - const PlaybackData = await Jellyfin.StatsSubmitCustomQuery(query); + const PlaybackData = await API.StatsSubmitCustomQuery(query); let DataToInsert = await PlaybackData.map(mappingPlaybackReporting); @@ -589,7 +589,7 @@ async function fullSync(triggertype) { return; } - let libraries = await Jellyfin.getLibraries(); + let libraries = await API.getLibraries(); if (libraries.length === 0) { syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." }); await logging.updateLog(syncTask.uuid, syncTask.loggedData, taskstate.FAILED); @@ -616,7 +616,7 @@ async function fullSync(triggertype) { message: wsMessage, }); - let libraryItems = await Jellyfin.getItemsFromParentId({ + let libraryItems = await API.getItemsFromParentId({ id: item.Id, ws: sendUpdate, syncTask: syncTask, @@ -697,7 +697,7 @@ async function partialSync(triggertype) { return; } - const libraries = await Jellyfin.getLibraries(); + const libraries = await API.getLibraries(); if (libraries.length === 0) { syncTask.loggedData.push({ Message: "Error: No Libararies found to sync." }); @@ -720,7 +720,7 @@ async function partialSync(triggertype) { type: "Update", message: "Fetching Data for Library : " + library.Name + ` (${i + 1}/${filtered_libraries.length})`, }); - let recentlyAddedForLibrary = await Jellyfin.getRecentlyAdded({ libraryid: library.Id, limit: 10 }); + let recentlyAddedForLibrary = await API.getRecentlyAdded({ libraryid: library.Id, limit: 10 }); sendUpdate(syncTask.wsKey, { type: "Update", message: "Mapping Data for Library : " + library.Name }); const libraryItemsWithParent = recentlyAddedForLibrary.map((items) => ({ @@ -734,7 +734,7 @@ async function partialSync(triggertype) { const library_items = data.filter((item) => ["Movie", "Audio", "Series"].includes(item.Type)); for (const item of library_items.filter((item) => item.Type === "Series")) { - let dataForShow = await Jellyfin.getItemsFromParentId({ id: item.Id }); + let dataForShow = await API.getItemsFromParentId({ id: item.Id }); const seasons_and_episodes_for_show = dataForShow.filter((item) => ["Season", "Episode"].includes(item.Type)); data.push(...seasons_and_episodes_for_show); } @@ -860,14 +860,14 @@ router.post("/fetchItem", async (req, res) => { return; } - const libraries = await Jellyfin.getLibraries(); + const libraries = await API.getLibraries(); const item = []; for (let i = 0; i < libraries.length; i++) { const library = libraries[i]; - let libraryItems = await Jellyfin.getItemsFromParentId({ id: library.Id, itemid: itemId }); + let libraryItems = await API.getItemsFromParentId({ id: library.Id, itemid: itemId }); if (libraryItems.length > 0) { const libraryItemsWithParent = libraryItems.map((items) => ({ diff --git a/backend/server.js b/backend/server.js index 641c671..01fd27a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -51,6 +51,22 @@ app.set("trust proxy", 1); app.disable("x-powered-by"); app.use(compression()); +function typeInferenceMiddleware(req, res, next) { + Object.keys(req.query).forEach((key) => { + const value = req.query[key]; + if (value.toLowerCase() === "true" || value.toLowerCase() === "false") { + // Convert to boolean + req.query[key] = value.toLowerCase() === "true"; + } else if (!isNaN(value) && value.trim() !== "") { + // Convert to number if it's a valid number + req.query[key] = +value; + } + }); + next(); +} + +app.use(typeInferenceMiddleware); + // initiate routes app.use("/auth", authRouter, () => { /* #swagger.tags = ['Auth'] */ diff --git a/backend/tasks/ActivityMonitor.js b/backend/tasks/ActivityMonitor.js index 3e17e04..0ed3adb 100644 --- a/backend/tasks/ActivityMonitor.js +++ b/backend/tasks/ActivityMonitor.js @@ -5,11 +5,11 @@ const moment = require("moment"); const { columnsPlayback, mappingPlayback } = require("../models/jf_playback_activity"); const { jf_activity_watchdog_columns, jf_activity_watchdog_mapping } = require("../models/jf_activity_watchdog"); const configClass = require("../classes/config"); -const JellyfinAPI = require("../classes/jellyfin-api"); +const API = require("../classes/api-loader"); const { sendUpdate } = require("../ws"); async function ActivityMonitor(interval) { - const Jellyfin = new JellyfinAPI(); + // console.log("Activity Interval: " + interval); setInterval(async () => { @@ -20,7 +20,7 @@ async function ActivityMonitor(interval) { return; } const ExcludedUsers = config.settings?.ExcludedUsers || []; - const apiSessionData = await Jellyfin.getSessions(); + const apiSessionData = await API.getSessions(); const SessionData = apiSessionData.filter((row) => row.NowPlayingItem !== undefined && !ExcludedUsers.includes(row.UserId)); sendUpdate("sessions", apiSessionData); /////get data from jf_activity_monitor @@ -191,7 +191,7 @@ async function ActivityMonitor(interval) { /////////////////////////// } catch (error) { if (error?.code === "ECONNREFUSED") { - console.error("Error: Unable to connect to Jellyfin"); + console.error("Error: Unable to connect to API");//TO-DO Change this to correct API name } else if (error?.code === "ERR_BAD_RESPONSE") { console.warn(error.response?.data); } else { diff --git a/package-lock.json b/package-lock.json index 3321bbc..2ee4ee1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jfstat", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 79bbb08..da49494 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jfstat", - "version": "1.1.0", + "version": "1.1.1", "private": true, "main": "src/index.jsx", "scripts": { diff --git a/src/lib/config.jsx b/src/lib/config.jsx index a809660..4f439e3 100644 --- a/src/lib/config.jsx +++ b/src/lib/config.jsx @@ -8,8 +8,8 @@ async function Config() { Authorization: `Bearer ${token}`, }, }); - const { JF_HOST, APP_USER,REQUIRE_LOGIN, settings } = response.data; - return { hostUrl: JF_HOST, username: APP_USER, token:token, requireLogin:REQUIRE_LOGIN, settings:settings }; + const { JF_HOST, APP_USER,REQUIRE_LOGIN, settings, IS_JELLYFIN } = response.data; + return { hostUrl: JF_HOST, username: APP_USER, token:token, requireLogin:REQUIRE_LOGIN, settings:settings, IS_JELLYFIN:IS_JELLYFIN }; } catch (error) { // console.log(error); diff --git a/src/pages/components/general/last-watched-card.jsx b/src/pages/components/general/last-watched-card.jsx index 27a535c..3548e38 100644 --- a/src/pages/components/general/last-watched-card.jsx +++ b/src/pages/components/general/last-watched-card.jsx @@ -75,7 +75,7 @@ function LastWatchedCard(props) {