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

452 lines
12 KiB
JavaScript

const configClass = require("./config");
const { axios } = require("./axios");
class JellyfinAPI {
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("[JELLYFIN-API]: " + this.#httpErrorMessageHandler(error));
} else {
console.log("[JELLYFIN-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");
}
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 [];
}
}
async validateSettings(url, apikey) {
try {
const response = await axios.get(url, {
headers: {
"X-MediaBrowser-Token": apikey,
},
});
return {
isValid: response.status === 200,
errorMessage: "",
};
} catch (error) {
this.#errorHandler(error);
return {
isValid: false,
status: error?.response?.status ?? 0,
errorMessage:
error?.response != null
? this.#httpErrorMessageHandler(error)
: error.code == "ENOTFOUND"
? "Unable to connect. Please check the URL and your network connection."
: error.message,
};
}
}
}
module.exports = JellyfinAPI;