Files
Jellystat/backend/classes/emby-api.js

597 lines
16 KiB
JavaScript

const configClass = require("./config");
const { axios } = require("./axios");
class EmbyAPI {
constructor() {
this.config = null;
this.configReady = false;
this.#checkReadyStatus();
this.sessionErrorCounter = 0;
}
//Helper classes
#checkReadyStatus() {
let checkConfigError = setInterval(async () => {
const success = await this.#fetchConfig();
if (success) {
clearInterval(checkConfigError);
}
}, 5000); // Check every 5 seconds
}
async #fetchConfig() {
const _config = await new configClass().getConfig();
if (!_config.error && _config.state === 2) {
this.config = _config;
this.configReady = true;
return true;
}
return false;
}
#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);
if (stackTrace.length < 1) {
return "Unknown";
}
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) {
if (!error.stack) return [];
const stackTrace = error.stack.split("\n");
return stackTrace;
}
#delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
//Functions
async getUsers(refreshConfig = false) {
if (!this.configReady || refreshConfig) {
const success = await this.#fetchConfig();
if (!success) {
return [];
}
}
try {
const url = `${this.config.JF_HOST}/Users`;
const response = await axios.get(url, {
headers: {
"X-MediaBrowser-Token": this.config.JF_API_KEY,
},
});
if (Array.isArray(response?.data)) {
return response?.data || [];
}
console.log("[JELLYFIN-API] : getUsers - " + (response?.data || response));
return [];
} catch (error) {
this.#errorHandler(error);
return [];
}
}
async getAdmins(refreshConfig = false) {
try {
const users = await this.getUsers(refreshConfig);
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 limit = params && params.limit !== undefined ? params.limit : increment;
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,Genres",
startIndex: startIndex,
recursive: recursive,
limit: limit,
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 || final_response.length >= limit) {
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) {
const success = await this.#fetchConfig();
if (!success) {
return [];
}
}
try {
let url = `${this.config.JF_HOST}/Items?ParentId=${id}`;
let userid;
if (!userid || userid == null) {
await new configClass().getPreferedAdmin().then(async (adminid) => {
if (!adminid || adminid == null) {
userid = (await this.getAdmins())[0].Id;
} else {
userid = adminid;
}
});
}
url += `&userId=${userid}`;
if (itemid && itemid != null) {
url += `&Ids=${itemid}`;
}
let startIndex = params && params.startIndex !== undefined ? params.startIndex : 0;
let increment = params && params.increment !== undefined ? params.increment : 200;
let recursive = params && params.recursive !== undefined ? params.recursive : true;
let limit = params && params.limit !== undefined ? params.limit : increment;
let total = startIndex + increment;
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,Genres",
startIndex: startIndex,
recursive: recursive,
limit: limit,
isMissing: false,
excludeLocationTypes: "Virtual",
sortBy: "DateCreated",
sortOrder: "Descending",
},
});
total = response?.data?.TotalRecordCount || 0;
startIndex += increment;
const result = response?.data?.Items || [];
AllItems.push(...result);
if (ws && syncTask && wsMessage) {
ws(syncTask.wsKey, {
type: "Update",
message: `${wsMessage} - ${((Math.min(startIndex, total) / total) * 100).toFixed(2)}%`,
});
}
if (
response.data.TotalRecordCount === undefined ||
(params && params.startIndex !== undefined) ||
AllItems.length >= limit
) {
break;
}
await this.#delay(10);
}
return AllItems;
} catch (error) {
this.#errorHandler(error);
return [];
}
}
async getItemInfo({ itemID, userid }) {
if (!this.configReady) {
const success = await this.#fetchConfig();
if (!success) {
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) {
const success = await this.#fetchConfig();
if (!success) {
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) {
const success = await this.#fetchConfig();
if (!success) {
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) {
const success = await this.#fetchConfig();
if (!success) {
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) {
const success = await this.#fetchConfig();
if (!success) {
return [];
}
}
try {
if (!userid || userid == null) {
let adminid = await new configClass().getPreferedAdmin();
if (!adminid || adminid == null) {
const admins = await this.getAdmins();
if (admins.length > 0) {
userid = admins[0].Id;
}
} else {
userid = adminid;
}
}
if (!userid || userid == null) {
console.log("[JELLYFIN-API]: getRecentlyAdded - No Admins/UserIds found");
return [];
}
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,Genres",
},
});
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,
},
})
.then((response) => {
if (this.sessionErrorCounter > 0) {
console.log("[EMBY-API]: /sessions - Connection restored");
this.sessionErrorCounter = 0;
}
return response;
})
.catch((error) => {
if (this.sessionErrorCounter == 0) {
this.sessionErrorCounter++;
console.log("[EMBY-API]: /sessions - Unable to connect. Please check the URL and your network connection");
}
return { data: [] };
});
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) {
const success = await this.#fetchConfig();
if (!success) {
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) {
const success = await this.#fetchConfig();
if (!success) {
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;