mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-03-18 21:30:35 +01:00
Version Bump 1.1.0 => 1.1.1
Added Dynamic API loader framework for Emby/Jellyfin switching, Emby API is still WIP, DO NOT USE as per #133 Reworked ome pages for correct url mapping of emby external links Added IS_JELLYFIN flag to config endpoint to indicate if server is displaying Emby or Jellyfin Data Fix for #218 Require Login set to false still displays Login Page until reload New feat: Grouped Recently added Episodes under Seasons and Episode count on Home page. Toggle to revert back to ugrouped display will be added later Added middleware to infer param types in API to simplify value checks, eg bool or numeric parameters
This commit is contained in:
13
backend/classes/api-loader.js
Normal file
13
backend/classes/api-loader.js
Normal file
@@ -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();
|
||||
@@ -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" };
|
||||
|
||||
503
backend/classes/emby-api.js
Normal file
503
backend/classes/emby-api.js
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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'] */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jfstat",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jfstat",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"private": true,
|
||||
"main": "src/index.jsx",
|
||||
"scripts": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -75,7 +75,7 @@ function LastWatchedCard(props) {
|
||||
<div className="last-item-details">
|
||||
<div className="last-last-played">{`${i18next.t("USERS_PAGE.AGO_ALT")} ${formatTime(props.data.LastPlayed)} ${i18next.t("USERS_PAGE.AGO").toLocaleLowerCase()}`}</div>
|
||||
|
||||
<div className="pb-2">
|
||||
<div className="pb-2">1
|
||||
<Link to={`/users/${props.data.UserId}`}>{props.data.UserName}</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ function ItemInfo() {
|
||||
</h1>
|
||||
<Link
|
||||
className="px-2"
|
||||
to={config.hostUrl + "/web/index.html#!/details?id=" + (data.EpisodeId || data.Id)}
|
||||
to={config.hostUrl + `/web/index.html#!/${config.IS_JELLYFIN? "details" : "item"}?id=` + (data.EpisodeId || data.Id) + (config.settings.ServerID ? "&serverId=" + config.settings.ServerID : "")}
|
||||
title={i18next.t("ITEM_INFO.OPEN_IN_JELLYFIN")}
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Blurhash } from "react-blurhash";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import "../../../css/lastplayed.css";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
function RecentlyAddedCard(props) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
@@ -21,7 +22,7 @@ function RecentlyAddedCard(props) {
|
||||
|
||||
return (
|
||||
<div className="last-card">
|
||||
<Link to={`/libraries/item/${props.data.EpisodeId ?? props.data.Id}`}>
|
||||
<Link to={`/libraries/item/${(props.data.NewEpisodeCount != undefined ? props.data.SeasonId : props.data.EpisodeId) ?? props.data.Id}`}>
|
||||
<div className="last-card-banner">
|
||||
{loaded ? null : props.data.PrimaryImageHash ? (
|
||||
<Blurhash hash={props.data.PrimaryImageHash} width={"100%"} height={"100%"} className="rounded-3 overflow-hidden" />
|
||||
@@ -47,19 +48,29 @@ function RecentlyAddedCard(props) {
|
||||
<div className="last-item-name">
|
||||
<Link to={`/libraries/item/${props.data.SeriesId ?? props.data.Id}`}>{props.data.SeriesName ?? props.data.Name}</Link>
|
||||
</div>
|
||||
{props.data.Type === "Episode" ? (
|
||||
{props.data.Type === "Episode" && props.data.NewEpisodeCount == undefined && (
|
||||
<div className="last-item-episode">
|
||||
<Link to={`/libraries/item/${props.data.EpisodeId}`}>{props.data.Name}</Link>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.data.SeasonNumber ? (
|
||||
{props.data.SeasonNumber && props.data.NewEpisodeCount == undefined && (
|
||||
<div className="last-item-episode number">
|
||||
S{props.data.SeasonNumber} - E{props.data.EpisodeNumber}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{props.data.SeasonNumber && props.data.NewEpisodeCount != undefined && (
|
||||
<div className="last-item-episode number pt-0 pb-1">
|
||||
<Trans i18nKey="SEASON" /> {props.data.SeasonNumber}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.data.NewEpisodeCount && (
|
||||
<div className="last-item-episode number pt-0">
|
||||
{props.data.NewEpisodeCount} <Trans i18nKey="EPISODES" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,15 +10,18 @@ import { Trans } from "react-i18next";
|
||||
function RecentlyAdded(props) {
|
||||
const [data, setData] = useState();
|
||||
const token = localStorage.getItem("token");
|
||||
const groupRecentlyAdded = localStorage.getItem("groupRecentlyAdded") ?? true;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let url = `/api/getRecentlyAdded`;
|
||||
let url = `/api/getRecentlyAdded?GroupResults=${groupRecentlyAdded}`;
|
||||
if (props.LibraryId) {
|
||||
url += `?libraryid=${props.LibraryId}`;
|
||||
url += `&libraryid=${props.LibraryId}`;
|
||||
}
|
||||
|
||||
console.log(url);
|
||||
|
||||
const itemData = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -54,7 +54,7 @@ function Login() {
|
||||
.then(async (response) => {
|
||||
localStorage.setItem("token", response.data.token);
|
||||
setProcessing(false);
|
||||
if (JS_USERNAME) {
|
||||
if (JS_USERNAME || response.data.token) {
|
||||
setsubmitButtonText(i18next.t("SUCCESS"));
|
||||
window.location.reload();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user