// Websocket transport between the web UI and xTeVe.
//
// One socket stays open for the life of the page. Requests are queued and
// sent one at a time; each carries an id that the server echoes back, and
// the response is matched on it (or, for older servers that close after a
// single reply, to the oldest in-flight request). Connection loss triggers
// a reconnect with exponential backoff, and queued requests survive it.
class WSClient {
constructor() {
this.socket = null;
this.queue = [];
this.inFlight = null;
this.nextID = 1;
this.reconnectDelay = 500;
this.reconnectTimer = 0;
this.closedByUs = false;
this.requestTimeoutMs = 12000;
}
url() {
var protocol = window.location.protocol == "https:" ? "wss://" : "ws://";
var host = window.location.host;
if (host == undefined || host.length < 1) {
host = window.location.hostname;
}
return protocol + host + "/data/";
}
// send queues a command and resolves with the parsed response.
send(payload) {
var client = this;
return new Promise(function (resolve, reject) {
var req = {
id: String(client.nextID++),
payload: payload,
resolve: resolve,
reject: reject,
timer: 0,
sent: false
};
client.queue.push(req);
client.pump();
});
}
// hasQueued reports whether a command of this kind is already waiting.
hasQueued(cmd) {
if (this.inFlight != null && this.inFlight.payload["cmd"] == cmd) {
return true;
}
for (var i = 0; i < this.queue.length; i++) {
if (this.queue[i].payload["cmd"] == cmd) {
return true;
}
}
return false;
}
connect() {
if (this.socket != null && (this.socket.readyState == WebSocket.OPEN || this.socket.readyState == WebSocket.CONNECTING)) {
return;
}
var client = this;
var socket = new WebSocket(this.url());
this.socket = socket;
socket.onopen = function () {
WS_AVAILABLE = true;
client.reconnectDelay = 500;
client.pump();
};
socket.onmessage = function (e) {
client.handleMessage(e.data);
};
socket.onerror = function () {
// onclose follows; nothing to do here.
};
socket.onclose = function () {
if (client.socket == socket) {
client.socket = null;
}
// An in-flight request that never got its answer goes back to the
// front of the queue and is retried on the next connection.
if (client.inFlight != null) {
var req = client.inFlight;
client.inFlight = null;
req.sent = false;
client.queue.unshift(req);
}
client.scheduleReconnect();
};
}
scheduleReconnect() {
if (this.closedByUs || this.reconnectTimer != 0) {
return;
}
if (this.queue.length == 0) {
// Idle: reconnect lazily on the next request.
return;
}
var client = this;
this.reconnectTimer = window.setTimeout(function () {
client.reconnectTimer = 0;
client.connect();
}, this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 10000);
}
// pump sends the next queued request if the socket is open and idle.
pump() {
if (this.inFlight != null || this.queue.length == 0) {
return;
}
if (this.socket == null || this.socket.readyState != WebSocket.OPEN) {
this.connect();
return;
}
var req = this.queue.shift();
this.inFlight = req;
req.sent = true;
req.payload["id"] = req.id;
var client = this;
req.timer = window.setTimeout(function () {
if (client.inFlight == req) {
client.inFlight = null;
req.reject(new Error("timeout"));
// A socket that stopped answering is not trusted any more.
client.resetSocket();
client.pump();
}
}, this.requestTimeoutMs);
try {
this.socket.send(JSON.stringify(req.payload));
}
catch (err) {
window.clearTimeout(req.timer);
this.inFlight = null;
req.sent = false;
this.queue.unshift(req);
this.resetSocket();
}
}
resetSocket() {
var socket = this.socket;
this.socket = null;
if (socket != null) {
socket.onclose = null;
try {
socket.close();
}
catch (err) { /* ignore */ }
}
this.scheduleReconnect();
}
handleMessage(raw) {
var response;
try {
response = JSON.parse(raw);
}
catch (err) {
return;
}
var req = this.inFlight;
if (req == null) {
return;
}
// Servers that echo the id let us drop stray late answers.
if (response.hasOwnProperty("id") && response["id"] != req.id) {
return;
}
window.clearTimeout(req.timer);
this.inFlight = null;
req.resolve(response);
this.pump();
}
}
var WS = new WSClient();
class Server {
constructor(cmd) {
this.cmd = cmd;
}
request(data) {
var isLogUpdate = this.cmd == "updateLog";
// Log polling must never pile up behind a stalled connection.
if (isLogUpdate && WS.hasQueued("updateLog")) {
return;
}
if (!isLogUpdate) {
showElement("loading", true);
UNDO = new Object();
setConnectionState("busy");
}
data["cmd"] = this.cmd;
var requestCmd = this.cmd;
WS.send(data).then(function (response) {
WS_FAILURE_COUNT = 0;
if (!isLogUpdate) {
showElement("loading", false);
}
setConnectionState("online");
if (response["status"] == false) {
setConnectionState("offline");
alertUser(response["err"]);
if (response.hasOwnProperty("reload")) {
location.reload();
}
return;
}
if (response.hasOwnProperty("logoURL")) {
var div = document.getElementById("channel-icon");
div.value = response["logoURL"];
div.className = "changed";
return;
}
switch (requestCmd) {
case "updateLog":
SERVER["log"] = response["log"];
if (document.getElementById("content_log")) {
showLogs(false);
}
return;
default:
SERVER = response;
break;
}
if (response.hasOwnProperty("openMenu")) {
var menu = document.getElementById(response["openMenu"]);
menu.click();
showElement("popup", false);
}
if (response.hasOwnProperty("openLink")) {
window.location = response["openLink"];
}
if (response.hasOwnProperty("alert")) {
alertUser(response["alert"]);
}
if (response.hasOwnProperty("reload")) {
location.reload();
}
if (response.hasOwnProperty("wizard")) {
createLayout();
configurationWizard[response["wizard"]].createWizard();
return;
}
createLayout();
}, function (err) {
WS_FAILURE_COUNT++;
if (!isLogUpdate) {
showElement("loading", false);
}
var state = "offline";
if (isLogUpdate && WS_FAILURE_COUNT < 2) {
state = "idle";
}
setConnectionState(state);
if (WS_AVAILABLE == false && !isLogUpdate && requestCmd != "getServerConfig") {
alertUser("No websocket connection to xTeVe could be established. Check your network configuration.");
}
});
}
}
var WS_FAILURE_COUNT = 0;
class MainMenu {
constructor() {
this.DocumentID = "main-menu";
this.HTMLTag = "LI";
this.ImagePath = "img/";
}
createIMG(src) {
var element = document.createElement("IMG");
element.setAttribute("src", this.ImagePath + src);
return element;
}
createValue(value) {
var element = document.createElement("P");
element.textContent = value;
return element;
}
}
class MainMenuItem extends MainMenu {
constructor(menuKey, value, image, headline) {
super();
this.menuKey = menuKey;
this.value = value;
this.imgSrc = image;
this.headline = headline;
}
createItem() {
var item = document.createElement("LI");
item.addEventListener("click", function () { openThisMenu(item); });
item.setAttribute("id", this.id);
item.setAttribute("data-menu", this.menuKey);
var img = this.createIMG(this.imgSrc);
var value = this.createValue(this.value);
item.appendChild(img);
item.appendChild(value);
var doc = document.getElementById(this.DocumentID);
doc.appendChild(item);
switch (this.menuKey) {
case "playlist":
this.tableHeader = ["Playlist", "Tuner", "Last Update", "Availability %", "Type", "Streams", "group-title %", "tvg-id %", "Unique ID %"];
break;
case "xmltv":
this.tableHeader = ["Guide", "Last Update", "Availability %", "Channels", "Programs"];
break;
case "filter":
this.tableHeader = ["Filter Name", "Filter Type", "Filter"];
break;
case "users":
this.tableHeader = ["Username", "Password", "WEB", "PMS", "M3U", "XML", "API"];
break;
case "mapping":
this.tableHeader = ["BULK", "Ch. No.", "Logo", "Channel Name", "Playlist", "Group Title", "XMLTV File", "XMLTV ID", "Edit"];
break;
}
//console.log(this.menuKey, this.tableHeader);
}
}
var SHELL_LAYOUT_READY = false;
function setLayoutMenuState(open) {
if (document.body == null) {
return;
}
if (open == true) {
document.body.classList.add("menu-open");
}
else {
document.body.classList.remove("menu-open");
}
}
function toggleLayoutMenu() {
if (document.body == null) {
return;
}
var isOpen = document.body.classList.contains("menu-open");
setLayoutMenuState(!isOpen);
}
function closeLayoutMenuIfMobile() {
if (window.innerWidth <= 900) {
setLayoutMenuState(false);
}
}
function setActiveMenu(menuID) {
ACTIVE_MENU_ID = menuID.toString();
var menu = document.getElementById("main-menu");
if (menu == null) {
return;
}
var items = menu.getElementsByTagName("LI");
for (var i = 0; i < items.length; i++) {
items[i].classList.remove("menu-active");
}
var activeItem = document.getElementById(ACTIVE_MENU_ID);
var activeMenuKey = "";
if (activeItem != null) {
activeItem.classList.add("menu-active");
var menuKeyValue = activeItem.getAttribute("data-menu");
if (menuKeyValue != null) {
activeMenuKey = menuKeyValue;
}
}
if (document.body != null) {
if (activeMenuKey.length > 0) {
document.body.setAttribute("data-active-menu", activeMenuKey);
}
else {
document.body.removeAttribute("data-active-menu");
}
if (activeMenuKey == "log") {
document.body.classList.add("menu-log-focus");
}
else {
document.body.classList.remove("menu-log-focus");
}
}
}
function renderStatusCards() {
var wrapper = document.getElementById("status-cards");
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
return;
}
var info = SERVER["clientInfo"];
var errors = parseInt(info["errors"], 10);
var warnings = parseInt(info["warnings"], 10);
var cards = [
{ label: "Streams", value: info["streams"], tone: "ok" },
{ label: "EPG Source", value: info["epgSource"], tone: "neutral" },
{ label: "XEPG Channels", value: info["xepg"], tone: "ok" },
{ label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok" },
{ label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok" },
{ label: "DVR", value: info["DVR"], tone: "neutral" },
];
wrapper.innerHTML = "";
cards.forEach(card => {
var box = document.createElement("DIV");
box.className = "status-card status-card-" + card.tone;
var label = document.createElement("P");
label.className = "status-card-label";
label.innerText = card.label;
var value = document.createElement("P");
value.className = "status-card-value";
if (card.value == undefined || card.value == "") {
value.innerText = "-";
}
else {
value.innerText = card.value;
}
box.appendChild(label);
box.appendChild(value);
wrapper.appendChild(box);
});
}
function initShellLayout() {
if (SHELL_LAYOUT_READY == true) {
return;
}
var toggle = document.getElementById("menu-toggle");
if (toggle != null) {
toggle.onclick = function () {
toggleLayoutMenu();
};
}
var overlay = document.getElementById("layout-overlay");
if (overlay != null) {
overlay.onclick = function () {
setLayoutMenuState(false);
};
}
document.addEventListener("keydown", function (event) {
if (event.key == "Escape") {
setLayoutMenuState(false);
showElement("popup", false);
return;
}
if (event.key == "/") {
var target = event.target;
var onInput = target.tagName == "INPUT" || target.tagName == "TEXTAREA" || target.tagName == "SELECT";
if (onInput == true) {
return;
}
var search = document.getElementById("searchMapping");
if (search != null) {
event.preventDefault();
search.focus();
}
}
});
setConnectionState("idle");
SHELL_LAYOUT_READY = true;
}
function shouldPollLogs() {
if (document.hidden == true) {
return false;
}
if (document.getElementById("content_log") == null) {
return false;
}
if (ACTIVE_MENU_ID.length < 1) {
return false;
}
var activeItem = document.getElementById(ACTIVE_MENU_ID);
if (activeItem == null) {
return false;
}
return activeItem.getAttribute("data-menu") == "log";
}
function PageReady() {
initShellLayout();
var server = new Server("getServerConfig");
server.request(new Object());
var bootstrapAttempts = 0;
var maxBootstrapAttempts = 5;
var bootstrapTimer = window.setInterval(function () {
if (SERVER.hasOwnProperty("clientInfo") == true) {
window.clearInterval(bootstrapTimer);
return;
}
if (WS.hasQueued("getServerConfig")) {
return;
}
bootstrapAttempts++;
var retryServer = new Server("getServerConfig");
retryServer.request(new Object());
if (bootstrapAttempts >= maxBootstrapAttempts) {
window.clearInterval(bootstrapTimer);
}
}, 3000);
window.addEventListener("resize", function () {
if (window.innerWidth > 900) {
setLayoutMenuState(false);
}
calculateWrapperHeight();
}, true);
setInterval(function () {
if (shouldPollLogs() == false) {
return;
}
updateLog();
}, 10000);
return;
}
function isClientInfoHttpURL(value) {
return /^https?:\/\//i.test(value);
}
function setClientInfoValue(key, value) {
var element = document.getElementById(key);
if (element == null) {
return;
}
var textValue = "";
if (value != undefined && value != null) {
textValue = String(value);
}
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
element.innerHTML = "";
var anchor = document.createElement("a");
anchor.href = textValue;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.textContent = textValue;
element.appendChild(anchor);
return;
}
element.textContent = textValue;
}
// menuItemVisible reports whether a menu entry applies to the current server state.
function menuItemVisible(item) {
switch (item.menuKey) {
case "users":
case "logout":
return SERVER["settings"]["authentication.web"] == true;
case "mapping":
case "xmltv":
return SERVER["clientInfo"]["epgSource"] == "XEPG";
default:
return true;
}
}
// renderedMenuKeys lists the menu entries currently in the document, in order.
function renderedMenuKeys(menu) {
var keys = [];
var items = menu.getElementsByTagName("LI");
for (var i = 0; i < items.length; i++) {
keys.push(items[i].getAttribute("data-menu"));
}
return keys;
}
function createLayout() {
// Client Info
var obj = SERVER["clientInfo"];
var keys = getObjKeys(obj);
for (var i = 0; i < keys.length; i++) {
setClientInfoValue(keys[i], obj[keys[i]]);
}
renderStatusCards();
var menu = document.getElementById("main-menu");
if (menu == null) {
return;
}
// Menu: rebuilt only when the set of visible entries changes, so that
// keyboard focus inside the list survives routine server responses.
var visibleKeys = [];
for (let i = 0; i < menuItems.length; i++) {
menuItems[i].id = i;
if (menuItemVisible(menuItems[i])) {
visibleKeys.push(menuItems[i].menuKey);
}
}
if (visibleKeys.join(",") != renderedMenuKeys(menu).join(",")) {
menu.innerHTML = "";
for (let i = 0; i < menuItems.length; i++) {
if (menuItemVisible(menuItems[i])) {
menuItems[i].createItem();
}
}
}
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
setActiveMenu(ACTIVE_MENU_ID);
}
var content = document.getElementById("content");
if (ACTIVE_MENU_ID.length == 0 && content != null) {
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
var firstItem = menu.getElementsByTagName("LI")[0];
if (firstItem != undefined) {
firstItem.click();
}
}
}
return;
}
class Content {
constructor() {
this.DocumentID = "content";
this.TableID = "content_table";
this.headerClass = "content_table_header";
this.interactionID = "content-interaction";
}
createHeadline(value) {
var element = document.createElement("H3");
element.textContent = value;
return element;
}
createHR() {
var element = document.createElement("HR");
return element;
}
createInteraction() {
var element = document.createElement("DIV");
element.setAttribute("id", this.interactionID);
return element;
}
createDIV() {
var element = document.createElement("DIV");
element.id = this.DivID;
return element;
}
createTABLE() {
var element = document.createElement("TABLE");
element.id = this.TableID;
return element;
}
createTableRow() {
var element = document.createElement("TR");
element.className = this.headerClass;
return element;
}
createTableContent(menuKey) {
var data = new Object();
var rows = new Array();
switch (menuKey) {
case "playlist":
var fileTypes = new Array("m3u", "hdhr");
fileTypes.forEach(fileType => {
data = SERVER["settings"]["files"][fileType];
var keys = getObjKeys(data);
keys.forEach(key => {
var tr = document.createElement("TR");
tr.id = key;
tr.addEventListener("click", function () { openPopUp(fileType, tr); });
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["name"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (SERVER["settings"]["buffer"] != "-") {
cell.value = data[key]["tuner"];
}
else {
cell.value = "-";
}
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["last.update"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["provider.availability"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["type"].toUpperCase();
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["compatibility"]["streams"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["compatibility"]["group.title"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["compatibility"]["tvg.id"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["compatibility"]["stream.id"];
tr.appendChild(cell.createCell());
rows.push(tr);
});
});
break;
case "filter":
delete SERVER["settings"]["filter"][-1];
data = SERVER["settings"]["filter"];
var keys = getObjKeys(data);
keys.forEach(key => {
var tr = document.createElement("TR");
tr.id = key;
tr.addEventListener("click", function () { openPopUp(data[key]["type"], tr); });
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["name"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
switch (data[key]["type"]) {
case "custom-filter":
cell.value = "Custom";
break;
case "group-title":
cell.value = "Group";
break;
case "regex-filter":
cell.value = "Regex";
break;
default:
break;
}
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["filter"];
tr.appendChild(cell.createCell());
rows.push(tr);
});
break;
case "xmltv":
var fileTypes = new Array("xmltv");
fileTypes.forEach(fileType => {
data = SERVER["settings"]["files"][fileType];
var keys = getObjKeys(data);
keys.forEach(key => {
var tr = document.createElement("TR");
tr.id = key;
tr.addEventListener("click", function () { openPopUp(fileType, tr); });
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["name"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["last.update"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["provider.availability"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["compatibility"]["xmltv.channels"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["compatibility"]["xmltv.programs"];
tr.appendChild(cell.createCell());
rows.push(tr);
});
});
break;
case "users":
var fileTypes = new Array("users");
fileTypes.forEach(fileType => {
data = SERVER[fileType];
var keys = getObjKeys(data);
keys.forEach(key => {
var tr = document.createElement("TR");
tr.id = key;
tr.addEventListener("click", function () { openPopUp(fileType, tr); });
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["data"]["username"];
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = "******";
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (data[key]["data"]["authentication.web"] == true) {
cell.value = "✓";
}
else {
cell.value = "-";
}
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (data[key]["data"]["authentication.pms"] == true) {
cell.value = "✓";
}
else {
cell.value = "-";
}
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (data[key]["data"]["authentication.m3u"] == true) {
cell.value = "✓";
}
else {
cell.value = "-";
}
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (data[key]["data"]["authentication.xml"] == true) {
cell.value = "✓";
}
else {
cell.value = "-";
}
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (data[key]["data"]["authentication.api"] == true) {
cell.value = "✓";
}
else {
cell.value = "-";
}
tr.appendChild(cell.createCell());
rows.push(tr);
});
});
break;
case "mapping":
BULK_EDIT = false;
createSearchObj();
checkUndo("epgMapping");
console.log("MAPPING");
data = SERVER["xepg"]["epgMapping"];
var keys = getObjKeys(data);
keys.forEach(key => {
var tr = document.createElement("TR");
tr.id = key;
switch (data[key]["x-active"]) {
case true:
tr.className = "activeEPG";
break;
case false:
tr.className = "notActiveEPG";
break;
}
// Bulk
var cell = new Cell();
cell.child = true;
cell.childType = "BULK";
cell.value = false;
tr.appendChild(cell.createCell());
// Kanalnummer
var cell = new Cell();
cell.child = true;
cell.childType = "INPUTCHANNEL";
cell.value = data[key]["x-channelID"];
tr.appendChild(cell.createCell());
// Logo
var cell = new Cell();
cell.child = true;
cell.childType = "IMG";
cell.imageURL = data[key]["tvg-logo"];
tr.appendChild(cell.createCell());
// Kanalname
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.className = data[key]["x-category"];
cell.value = data[key]["x-name"];
tr.appendChild(cell.createCell());
// Playlist
var cell = new Cell();
cell.child = true;
cell.childType = "P";
//cell.value = data[key]["_file.m3u.name"]
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name");
tr.appendChild(cell.createCell());
// Gruppe (group-title)
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["x-group-title"];
tr.appendChild(cell.createCell());
// XMLTV Datei
var cell = new Cell();
cell.child = true;
cell.childType = "P";
if (data[key]["x-xmltv-file"] != "-") {
cell.value = getValueFromProviderFile(data[key]["x-xmltv-file"], "xmltv", "name");
}
else {
cell.value = data[key]["x-xmltv-file"];
}
tr.appendChild(cell.createCell());
// XMLTV Kanal
var cell = new Cell();
cell.child = true;
cell.childType = "P";
//var value = str.substring(1, 4);
var value = data[key]["x-mapping"];
if (value.length > 20) {
value = data[key]["x-mapping"].substring(0, 20) + "...";
}
cell.value = value;
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "EDIT";
cell.value = "Edit";
var editTd = cell.createCell();
var editButton = editTd.firstChild;
editButton.addEventListener("click", function () { openPopUp("mapping", editButton); });
editButton.setAttribute("id", key);
editButton.setAttribute("aria-label", "Edit " + data[key]["x-name"]);
tr.appendChild(editTd);
rows.push(tr);
});
break;
case "settings":
alert();
break;
default:
console.log("Table content (menuKey):", menuKey);
break;
}
return rows;
}
}
class Cell {
createCell() {
var td = document.createElement("TD");
if (this.child == true) {
var element;
switch (this.childType) {
case "P":
element = document.createElement(this.childType);
element.textContent = this.value;
element.className = this.className;
break;
case "INPUT":
element = document.createElement(this.childType);
element.value = this.value;
element.type = "text";
break;
case "INPUTCHANNEL":
element = document.createElement("INPUT");
element.addEventListener("change", function () { changeChannelNumber(element); });
element.value = this.value;
element.type = "text";
break;
case "BULK":
element = document.createElement("INPUT");
element.checked = this.value;
element.type = "checkbox";
element.className = "bulk hideBulk";
element.addEventListener("click", function (event) {
scheduleChannelRangeSelection(element, event);
});
break;
case "BULK_HEAD":
element = document.createElement("INPUT");
element.checked = this.value;
element.type = "checkbox";
element.className = "bulk hideBulk";
element.addEventListener("click", function () { selectAllChannels(); });
break;
case "IMG":
element = document.createElement(this.childType);
element.setAttribute("src", this.imageURL);
if (this.imageURL != "") {
// A broken logo falls back to no image; once, so the empty src cannot loop.
element.addEventListener("error", function () { element.setAttribute("src", ""); }, { once: true });
}
break;
case "EDIT":
element = document.createElement("INPUT");
element.type = "button";
element.value = this.value;
element.className = "mapping-edit-button";
break;
}
td.appendChild(element);
}
else {
td.textContent = this.value;
}
if (this.onclick == true) {
td.addEventListener("click", this.onclickHandler);
td.className = "pointer";
}
if (this.tdClassName != undefined) {
td.className = this.tdClassName;
}
return td;
}
}
class ShowContent extends Content {
constructor(menuID) {
super();
this.menuID = menuID;
}
createInput(type, name, value) {
var input = document.createElement("INPUT");
input.setAttribute("type", type);
input.setAttribute("name", name);
input.setAttribute("value", value);
return input;
}
show() {
COLUMN_TO_SORT = -1;
// Alten Inhalt löschen
var doc = document.getElementById(this.DocumentID);
doc.innerHTML = "";
showPreview(false);
// Überschrift
var headline = menuItems[this.menuID].headline;
var menuKey = menuItems[this.menuID].menuKey;
var h = this.createHeadline(headline);
doc.appendChild(h);
var hr = this.createHR();
doc.appendChild(hr);
// Interaktion
var div = this.createInteraction();
doc.appendChild(div);
var interaction = document.getElementById(this.interactionID);
switch (menuKey) {
case "playlist":
var input = this.createInput("button", menuKey, "New");
input.setAttribute("id", "-");
input.addEventListener("click", function () { openPopUp("playlist"); });
interaction.appendChild(input);
break;
case "filter":
var input = this.createInput("button", menuKey, "New");
input.setAttribute("id", -1);
input.addEventListener("click", function (event) { openPopUp("filter", event.currentTarget); });
interaction.appendChild(input);
break;
case "xmltv":
var input = this.createInput("button", menuKey, "New");
input.setAttribute("id", "xmltv");
input.addEventListener("click", function () { openPopUp("xmltv"); });
interaction.appendChild(input);
break;
case "users":
var input = this.createInput("button", menuKey, "New");
input.setAttribute("id", "users");
input.addEventListener("click", function () { openPopUp("users"); });
interaction.appendChild(input);
break;
case "mapping":
showElement("loading", true);
var input = this.createInput("button", menuKey, "Save");
input.addEventListener("click", function () { savePopupData("mapping", "", false, 0); });
interaction.appendChild(input);
var input = this.createInput("button", menuKey, "Bulk Edit");
input.addEventListener("click", function () { bulkEdit(); });
interaction.appendChild(input);
var input = this.createInput("search", "search", "");
input.setAttribute("id", "searchMapping");
input.setAttribute("placeholder", "Search");
input.className = "search";
input.addEventListener("input", function () { searchInMapping(); });
interaction.appendChild(input);
break;
case "settings":
var input = this.createInput("button", menuKey, "Save");
input.addEventListener("click", function () { saveSettings(); });
interaction.appendChild(input);
var input = this.createInput("button", menuKey, "Backup");
input.addEventListener("click", function () { backup(); });
interaction.appendChild(input);
var input = this.createInput("button", menuKey, "Restore");
input.addEventListener("click", function () { restore(); });
interaction.appendChild(input);
var wrapper = document.createElement("DIV");
wrapper.setAttribute("id", "box-wrapper");
doc.appendChild(wrapper);
this.DivID = "content_settings";
var settings = this.createDIV();
wrapper.appendChild(settings);
showSettings();
return;
break;
case "log":
var input = this.createInput("button", menuKey, "Reset Logs");
input.addEventListener("click", function () { resetLogs(); });
interaction.appendChild(input);
var wrapper = document.createElement("DIV");
wrapper.setAttribute("id", "box-wrapper");
doc.appendChild(wrapper);
this.DivID = "content_log";
var logs = this.createDIV();
wrapper.appendChild(logs);
showLogs(true);
return;
break;
case "logout":
location.reload();
document.cookie = "Token= ; expires = Thu, 01 Jan 1970 00:00:00 GMT";
break;
default:
console.log("Show content (menuKey):", menuKey);
break;
}
// Tabelle erstellen (falls benötigt)
var tableHeader = menuItems[this.menuID].tableHeader;
if (tableHeader.length > 0) {
var wrapper = document.createElement("DIV");
doc.appendChild(wrapper);
wrapper.setAttribute("id", "box-wrapper");
var table = this.createTABLE();
wrapper.appendChild(table);
var header = this.createTableRow();
table.appendChild(header);
// Kopfzeile der Tablle
tableHeader.forEach(element => {
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = element;
if (element == "BULK") {
cell.childType = "BULK_HEAD";
cell.value = false;
}
if (menuKey == "mapping") {
if (element == "Ch. No.") {
cell.onclick = true;
cell.onclickHandler = function () { sortTable(1); };
cell.tdClassName = "sortThis";
}
if (element == "Channel Name") {
cell.onclick = true;
cell.onclickHandler = function () { sortTable(3); };
}
if (element == "Playlist") {
cell.onclick = true;
cell.onclickHandler = function () { sortTable(4); };
}
if (element == "Group Title") {
cell.onclick = true;
cell.onclickHandler = function () { sortTable(5); };
}
}
header.appendChild(cell.createCell());
});
table.appendChild(header);
// Inhalt der Tabelle
var rows = this.createTableContent(menuKey);
rows.forEach(tr => {
table.appendChild(tr);
});
}
switch (menuKey) {
case "mapping":
sortTable(1);
break;
case "filter":
showPreview(true);
sortTable(0);
break;
default:
COLUMN_TO_SORT = -1;
sortTable(0);
break;
}
showElement("loading", false);
}
}
function openThisMenu(element) {
var id = element.id;
var content = new ShowContent(id);
setActiveMenu(id);
content.show();
closeLayoutMenuIfMobile();
calculateWrapperHeight();
return;
}
class PopupWindow {
constructor() {
this.DocumentID = "popup-custom";
this.InteractionID = "interaction";
this.doc = document.getElementById(this.DocumentID);
}
createTitle(title) {
var td = document.createElement("TD");
td.className = "left";
td.innerHTML = title + ":";
return td;
}
createContent(element) {
var td = document.createElement("TD");
td.appendChild(element);
return td;
}
createInteraction() {
var div = document.createElement("div");
div.setAttribute("id", "popup-interaction");
div.className = "interaction";
this.doc.appendChild(div);
}
}
class PopupContent extends PopupWindow {
constructor() {
super(...arguments);
this.table = document.createElement("TABLE");
}
createHeadline(headline) {
this.doc.innerHTML = "";
var element = document.createElement("H3");
element.innerHTML = headline.toUpperCase();
this.doc.appendChild(element);
// Tabelle erstellen
this.table = document.createElement("TABLE");
this.doc.appendChild(this.table);
}
appendRow(title, element) {
var tr = document.createElement("TR");
// Bezeichnung
if (title.length != 0) {
tr.appendChild(this.createTitle(title));
}
// Content
tr.appendChild(this.createContent(element));
this.table.appendChild(tr);
}
createInput(type, name, value) {
var input = document.createElement("INPUT");
if (value == undefined) {
value = "";
}
input.setAttribute("type", type);
input.setAttribute("name", name);
input.setAttribute("value", value);
return input;
}
createCheckbox(name) {
var input = document.createElement("INPUT");
input.setAttribute("type", "checkbox");
input.setAttribute("name", name);
return input;
}
createSelect(text, values, set, dbKey) {
var select = document.createElement("SELECT");
select.setAttribute("name", dbKey);
for (let i = 0; i < text.length; i++) {
var option = document.createElement("OPTION");
option.setAttribute("value", values[i]);
option.innerText = text[i];
select.appendChild(option);
}
if (set != "") {
select.value = set;
}
if (set == undefined) {
select.value = values[0];
}
return select;
}
selectOption(select, value) {
//select.selectedOptions = value
var s = select;
s.options[s.selectedIndex].value = value;
return select;
}
// isHTML must only be true for static text written in the source; never for data.
description(value, isHTML = false) {
var tr = document.createElement("TR");
var td = document.createElement("TD");
var span = document.createElement("PRE");
if (isHTML) {
span.innerHTML = value;
}
else {
span.textContent = value;
}
tr.appendChild(td);
tr.appendChild(this.createContent(span));
this.table.appendChild(tr);
}
// Interaktion
addInteraction(element) {
var interaction = document.getElementById("popup-interaction");
interaction.appendChild(element);
}
}
// Buffer choices a playlist can make; "default" means the global setting.
const PLAYLIST_BUFFER_OPTIONS = [
{ text: "Default (global setting)", value: "default" },
{ text: "No buffer (pass the URL through)", value: "-" },
{ text: "xTeVe", value: "xteve" },
{ text: "FFmpeg", value: "ffmpeg" },
{ text: "VLC", value: "vlc" }
];
function createBufferSelect(content, data) {
var text = PLAYLIST_BUFFER_OPTIONS.map(function (o) { return o.text; });
var values = PLAYLIST_BUFFER_OPTIONS.map(function (o) { return o.value; });
var current = data["buffer"];
if (values.indexOf(current) == -1) {
current = "default";
}
return content.createSelect(text, values, current, "buffer");
}
// playlistBufferActive: is a buffer in use for this playlist (its own choice,
// or the global setting when it has none)? Decides whether the tuner count
// can be edited.
function playlistBufferActive(data) {
var own = data["buffer"];
if (own != undefined && own != "default" && own != "") {
return own != "-";
}
return SERVER["settings"]["buffer"] != "-";
}
function openPopUp(dataType, element) {
var data = new Object();
var id;
switch (element) {
case undefined:
switch (dataType) {
case "group-title":
if (id == undefined) {
id = -1;
}
data = getLocalData("filter", id);
data["type"] = "group-title";
break;
case "custom-filter":
if (id == undefined) {
id = -1;
}
data = getLocalData("filter", id);
data["type"] = "custom-filter";
break;
case "regex-filter":
if (id == undefined) {
id = -1;
}
data = getLocalData("filter", id);
data["type"] = "regex-filter";
break;
default:
data["id.provider"] = "-";
data["type"] = dataType;
id = "-";
break;
}
break;
default:
id = element.id;
data = getLocalData(dataType, id);
break;
}
var content = new PopupContent();
switch (dataType) {
case "playlist":
content.createHeadline("Playlist type");
// Type
var text = ["M3U", "HDHomeRun"];
var values = ["m3u", "hdhr"];
var select = content.createSelect(text, values, "", "type");
select.setAttribute("id", "type");
content.appendRow("Type", select);
const typeSelect = select;
// Interaktion
content.createInteraction();
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Weiter
var input = content.createInput("button", "next", "Next");
input.addEventListener("click", function () { openPopUp(typeSelect.value); });
input.setAttribute("id", 'next');
content.addInteraction(input);
break;
case "m3u":
content.createHeadline(dataType);
// Name
var dbKey = "name";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Playlist name");
content.appendRow("Name", input);
// Beschreibung
var dbKey = "description";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Description");
content.appendRow("Description", input);
// URL
var dbKey = "file.source";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "File path or URL of the M3U");
content.appendRow("M3U File", input);
// Buffer (per playlist override of the global setting)
var bufferSelect = createBufferSelect(content, data);
content.appendRow("Buffer", bufferSelect);
// Tuner
if (playlistBufferActive(data)) {
var text = new Array();
var values = new Array();
for (var i = 1; i <= 100; i++) {
text.push(i.toString());
values.push(i.toString());
}
var dbKey = "tuner";
var select = content.createSelect(text, values, data[dbKey], dbKey);
content.appendRow("Tuner / Streams", select);
}
else {
var dbKey = "tuner";
if (data[dbKey] == undefined) {
data[dbKey] = 1;
}
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("readonly", "true");
input.className = "notAvailable";
content.appendRow("Tuner / Streams", input);
}
content.description("Number of parallel connections that can be established to the provider.
Only available with activated buffer.
New settings will only be applied after quitting all streams.", true);
// Interaktion
content.createInteraction();
// Löschen
if (data["id.provider"] != "-") {
var input = content.createInput("button", "delete", "Delete");
input.className = "delete";
input.addEventListener("click", function () { savePopupData("m3u", String(id), true, 0); });
content.addInteraction(input);
}
else {
var input = content.createInput("button", "back", "Back");
input.addEventListener("click", function () { openPopUp("playlist"); });
content.addInteraction(input);
}
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Aktualisieren
if (data["id.provider"] != "-") {
var input = content.createInput("button", "update", "Update");
input.addEventListener("click", function () { savePopupData("m3u", String(id), false, 1); });
content.addInteraction(input);
}
// Speichern
var input = content.createInput("button", "save", "Save");
input.addEventListener("click", function () { savePopupData("m3u", String(id), false, 0); });
content.addInteraction(input);
break;
case "hdhr":
content.createHeadline(dataType);
// Name
var dbKey = "name";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Playlist name");
content.appendRow("Name", input);
// Beschreibung
var dbKey = "description";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Description");
content.appendRow("Description", input);
// URL
var dbKey = "file.source";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "IP address and port (192.168.1.10:5004)");
content.appendRow("HDHomeRun IP", input);
// Buffer (per playlist override of the global setting)
var bufferSelect = createBufferSelect(content, data);
content.appendRow("Buffer", bufferSelect);
// Tuner
if (playlistBufferActive(data)) {
var text = new Array();
var values = new Array();
for (var i = 1; i <= 100; i++) {
text.push(i.toString());
values.push(i.toString());
}
var dbKey = "tuner";
var select = content.createSelect(text, values, data[dbKey], dbKey);
content.appendRow("Tuner / Streams", select);
}
else {
var dbKey = "tuner";
if (data[dbKey] == undefined) {
data[dbKey] = 1;
}
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("readonly", "true");
input.className = "notAvailable";
content.appendRow("Tuner / Streams", input);
}
content.description("Number of parallel connections that can be established to the provider.
Only available with activated buffer.
New settings will only be applied after quitting all streams.", true);
// Interaktion
content.createInteraction();
// Löschen
if (data["id.provider"] != "-") {
var input = content.createInput("button", "delete", "Delete");
input.addEventListener("click", function () { savePopupData("hdhr", String(id), true, 0); });
input.className = "delete";
content.addInteraction(input);
}
else {
var input = content.createInput("button", "back", "Back");
input.addEventListener("click", function () { openPopUp("playlist"); });
content.addInteraction(input);
}
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Aktualisieren
if (data["id.provider"] != "-") {
var input = content.createInput("button", "update", "Update");
input.addEventListener("click", function () { savePopupData("hdhr", String(id), false, 1); });
content.addInteraction(input);
}
// Speichern
var input = content.createInput("button", "save", "Save");
input.addEventListener("click", function () { savePopupData("hdhr", String(id), false, 0); });
content.addInteraction(input);
break;
case "filter":
content.createHeadline(dataType);
// Type
var dbKey = "type";
var text = ["M3U: " + "Group Title", "xTeVe: " + "Custom Filter", "xTeVe: " + "Regular Expression"];
var values = ["group-title", "custom-filter", "regex-filter"];
var select = content.createSelect(text, values, "group-title", dbKey);
select.setAttribute("id", id);
content.appendRow("Type", select);
const filterTypeSelect = select;
// Interaktion
content.createInteraction();
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Weiter
var input = content.createInput("button", "next", "Next");
input.addEventListener("click", function () { openPopUp(filterTypeSelect.value); });
input.setAttribute("id", 'next');
content.addInteraction(input);
break;
case "custom-filter":
case "group-title":
case "regex-filter":
switch (dataType) {
case "custom-filter":
content.createHeadline("Custom");
break;
case "group-title":
content.createHeadline("Group");
break;
case "regex-filter":
content.createHeadline("Regular Expression");
break;
}
// Name
var dbKey = "name";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Filter name");
content.appendRow("Filter Name", input);
// Beschreibung
var dbKey = "description";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Description");
content.appendRow("Description", input);
// Typ
var dbKey = "type";
var input = content.createInput("hidden", dbKey, data[dbKey]);
content.appendRow("", input);
var filterType = data[dbKey];
switch (filterType) {
case "custom-filter":
// Groß- Kleinschreibung beachten
var dbKey = "caseSensitive";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("Case Sensitive", input);
// Filterregel (Benutzerdefiniert)
var dbKey = "filter";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Sport {HD} !{ES,IT}");
content.appendRow("Filter Rule", input);
break;
case "regex-filter":
// Groß- Kleinschreibung beachten
var dbKey = "caseSensitive";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("Case Sensitive", input);
// Regulärer Ausdruck
var dbKey = "filter";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "^(BBC|ITV) \\d+$");
content.appendRow("Regular Expression", input);
content.description("Go regular expression, tried against the channel name, then the group title, then the raw M3U attributes. Case-insensitive unless Case Sensitive is checked. Invalid patterns are rejected on save.");
break;
case "group-title":
//alert(dbKey + " " + filterType)
// Filter basierend auf den Gruppen in der M3U
var dbKey = "filter";
var groupsM3U = getLocalData("m3uGroups", "");
var text = groupsM3U["text"];
var values = groupsM3U["value"];
var select = content.createSelect(text, values, data[dbKey], dbKey);
markChangedOnChange(select);
content.appendRow("Group Title", select);
content.description("Select a M3U group. (Counter)
Changing the group title in the M3U invalidates the filter.", true);
// Groß- Kleinschreibung beachten
var dbKey = "caseSensitive";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("Case Sensitive", input);
var dbKey = "include";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "FHD,UHD");
content.appendRow("Include", input);
content.description("Channel name must include.
(Comma separated) Comma means or", true);
var dbKey = "exclude";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "ES,IT");
content.appendRow("Exclude", input);
content.description("Channel name must not contain.
(Comma separated) Comma means or", true);
break;
default:
break;
}
// Interaktion
content.createInteraction();
// Löschen
var input = content.createInput("button", "delete", "Delete");
input.addEventListener("click", function () { savePopupData("filter", String(id), true, 0); });
input.className = "delete";
content.addInteraction(input);
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Speichern
var input = content.createInput("button", "save", "Save");
input.addEventListener("click", function () { savePopupData("filter", String(id), false, 0); });
content.addInteraction(input);
break;
case "xmltv":
content.createHeadline(dataType);
// Name
var dbKey = "name";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Guide name");
content.appendRow("Name", input);
// Beschreibung
var dbKey = "description";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Description");
content.appendRow("Description", input);
// URL
var dbKey = "file.source";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "File path or URL of the XMLTV");
content.appendRow("XMLTV File", input);
// Interaktion
content.createInteraction();
// Löschen
if (data["id.provider"] != "-") {
var input = content.createInput("button", "delete", "Delete");
input.addEventListener("click", function () { savePopupData("xmltv", String(id), true, 0); });
input.className = "delete";
content.addInteraction(input);
}
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Aktualisieren
if (data["id.provider"] != "-") {
var input = content.createInput("button", "update", "Update");
input.addEventListener("click", function () { savePopupData("xmltv", String(id), false, 1); });
content.addInteraction(input);
}
// Speichern
var input = content.createInput("button", "save", "Save");
input.addEventListener("click", function () { savePopupData("xmltv", String(id), false, 0); });
content.addInteraction(input);
break;
case "users":
content.createHeadline("Users");
// Benutzername
var dbKey = "username";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Username");
content.appendRow("Username", input);
// Neues Passwort
var dbKey = "password";
var input = content.createInput("password", dbKey, "");
input.setAttribute("placeholder", "Password");
content.appendRow("Password", input);
// Bestätigung
var dbKey = "confirm";
var input = content.createInput("password", dbKey, "");
input.setAttribute("placeholder", "Password confirm");
content.appendRow("Confirm", input);
// Berechtigung WEB
var dbKey = "authentication.web";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
if (data["defaultUser"] == true) {
input.addEventListener("click", function (event) { event.preventDefault(); });
}
content.appendRow("Web Access", input);
// Berechtigung PMS
var dbKey = "authentication.pms";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("PMS Access", input);
// Berechtigung M3U
var dbKey = "authentication.m3u";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("M3U Access", input);
// Berechtigung XML
var dbKey = "authentication.xml";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("XML Access", input);
// Berechtigung API
var dbKey = "authentication.api";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
content.appendRow("API Access", input);
// Interaktion
content.createInteraction();
// Löschen
if (data["defaultUser"] != true && id != "-") {
var input = content.createInput("button", "delete", "Delete");
input.className = "delete";
input.addEventListener("click", function () { savePopupData(dataType, String(id), true, 0); });
content.addInteraction(input);
}
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Speichern
var input = content.createInput("button", "save", "Save");
input.addEventListener("click", function () { savePopupData(dataType, String(id), false, 0); });
content.addInteraction(input);
break;
case "mapping":
content.createHeadline("Mapping");
// Aktiv
var dbKey = "x-active";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
input.id = "active";
//markChangedOnChange(input)
input.addEventListener("change", function () { toggleChannelStatus(id); });
content.appendRow("Active", input);
// Kanalname
var dbKey = "x-name";
var input = content.createInput("text", dbKey, data[dbKey]);
markChangedOnChange(input);
if (BULK_EDIT == true) {
input.style.border = "solid 1px red";
input.setAttribute("readonly", "true");
}
content.appendRow("Channel Name", input);
content.description(data["name"]);
// Beschreibung
var dbKey = "x-description";
var input = content.createInput("text", dbKey, data[dbKey]);
input.setAttribute("placeholder", "Used by the Dummy as an XML description");
markChangedOnChange(input);
content.appendRow("Channel Description", input);
// Aktualisierung des Kanalnamens
if (data.hasOwnProperty("_uuid.key")) {
if (data["_uuid.key"] != "") {
var dbKey = "x-update-channel-name";
var input = content.createCheckbox(dbKey);
markChangedOnChange(input);
input.checked = data[dbKey];
content.appendRow("Update Channel Name", input);
}
}
// Logo URL (Kanal)
var dbKey = "tvg-logo";
var input = content.createInput("text", dbKey, data[dbKey]);
markChangedOnChange(input);
input.setAttribute("id", "channel-icon");
content.appendRow("Logo URL", input);
// Aktualisierung des Kanallogos
var dbKey = "x-update-channel-icon";
var input = content.createCheckbox(dbKey);
input.checked = data[dbKey];
input.setAttribute("id", "update-icon");
markChangedOnChange(input, function () { changeChannelLogo(id); });
content.appendRow("Update Channel Logo", input);
// Erweitern der EPG Kategorie
var dbKey = "x-category";
var text = ["-", "Kids (Emby only)", "News", "Movie", "Series", "Sports"];
var values = ["", "Kids", "News", "Movie", "Series", "Sports"];
var select = content.createSelect(text, values, data[dbKey], dbKey);
markChangedOnChange(select);
content.appendRow("EPG Category", select);
// M3U Gruppentitel
var dbKey = "x-group-title";
var input = content.createInput("text", dbKey, data[dbKey]);
markChangedOnChange(input);
content.appendRow("Group Title (xteve.m3u)", input);
if (data["group-title"] != undefined) {
content.description(data["group-title"]);
}
// XMLTV Datei
var dbKey = "x-xmltv-file";
var xmlFile = data[dbKey];
var xmltv = new XMLTVFile();
var select = xmltv.getFiles(data[dbKey]);
select.setAttribute("name", dbKey);
select.setAttribute("id", "popup-xmltv");
markChangedOnChange(select, function (element) { setXmltvChannel(id, element); });
content.appendRow("XMLTV File", select);
var file = data[dbKey];
// XMLTV Mapping
var dbKey = "x-mapping";
var xmltv = new XMLTVFile();
var select = xmltv.getPrograms(file, data[dbKey]);
select.setAttribute("name", dbKey);
select.setAttribute("id", "popup-mapping");
markChangedOnChange(select, function (element) { checkXmltvChannel(id, element, xmlFile); });
sortSelect(select);
content.appendRow("XMLTV Channel", select);
// Interaktion
content.createInteraction();
// Logo hochladen
var input = content.createInput("button", "cancel", "Upload Logo");
input.addEventListener("click", function () { uploadLogo(); });
content.addInteraction(input);
// Abbrechen
var input = content.createInput("button", "cancel", "Cancel");
input.addEventListener("click", function () { showElement("popup", false); });
content.addInteraction(input);
// Fertig
var ids = new Array();
ids = getAllSelectedChannels();
if (ids.length == 0) {
ids.push(id);
}
var input = content.createInput("button", "save", "Done");
input.addEventListener("click", function () { donePopupData(dataType, ids.join(",")); });
content.addInteraction(input);
break;
default:
break;
}
showPopUpElement('popup-custom');
}
function savePopupData(dataType, id, remove, option) {
if (dataType == "mapping") {
var data = new Object();
console.log("Save mapping data");
cmd = "saveEpgMapping";
data["epgMapping"] = SERVER["xepg"]["epgMapping"];
console.log("SEND TO SERVER");
var server = new Server(cmd);
server.request(data);
delete UNDO["epgMapping"];
return;
}
console.log("Save popup data");
var div = document.getElementById("popup-custom");
var inputs = div.getElementsByTagName("TABLE")[0].getElementsByTagName("INPUT");
var selects = div.getElementsByTagName("TABLE")[0].getElementsByTagName("SELECT");
var input = new Object();
var confirmMsg;
for (let i = 0; i < selects.length; i++) {
var name;
name = selects[i].name;
var value = selects[i].value;
switch (name) {
case "tuner":
input[name] = parseInt(value);
break;
default:
input[name] = value;
break;
}
}
for (let i = 0; i < inputs.length; i++) {
switch (inputs[i].type) {
case "checkbox":
name = inputs[i].name;
input[name] = inputs[i].checked;
break;
case "text":
case "hidden":
case "password":
name = inputs[i].name;
switch (name) {
case "tuner":
input[name] = parseInt(inputs[i].value);
break;
default:
input[name] = inputs[i].value;
break;
}
break;
}
}
var data = new Object();
var cmd;
if (remove == true) {
input["delete"] = true;
}
switch (dataType) {
case "users":
confirmMsg = "Delete this user?";
if (id == "-") {
cmd = "saveNewUser";
data["userData"] = input;
}
else {
cmd = "saveUserData";
var d = new Object();
d[id] = input;
data["userData"] = d;
}
break;
case "m3u":
confirmMsg = "Delete this playlist?";
switch (option) {
// Popup: Save
case 0:
cmd = "saveFilesM3U";
break;
// Popup: Update
case 1:
cmd = "updateFileM3U";
break;
}
data["files"] = new Object;
data["files"][dataType] = new Object;
data["files"][dataType][id] = input;
break;
case "hdhr":
confirmMsg = "Delete this HDHomeRun tuner?";
switch (option) {
// Popup: Save
case 0:
cmd = "saveFilesHDHR";
break;
// Popup: Update
case 1:
cmd = "updateFileHDHR";
break;
}
data["files"] = new Object;
data["files"][dataType] = new Object;
data["files"][dataType][id] = input;
break;
case "xmltv":
confirmMsg = "Delete this XMLTV file?";
switch (option) {
// Popup: Save
case 0:
cmd = "saveFilesXMLTV";
break;
// Popup: Update
case 1:
cmd = "updateFileXMLTV";
break;
}
data["files"] = new Object;
data["files"][dataType] = new Object;
data["files"][dataType][id] = input;
break;
case "filter":
confirmMsg = "Delete this filter?";
cmd = "saveFilter";
data["filter"] = new Object;
data["filter"][id] = input;
break;
default:
console.log(dataType, id);
return;
break;
}
if (remove == true) {
if (!confirm(confirmMsg)) {
showElement("popup", false);
return;
}
}
console.log("SEND TO SERVER");
console.log(data);
var server = new Server(cmd);
server.request(data);
}
function donePopupData(dataType, idsStr) {
var ids = idsStr.split(',');
var div = document.getElementById("popup-custom");
var inputs = div.getElementsByClassName("changed");
ids.forEach(id => {
var input = new Object();
input = SERVER["xepg"]["epgMapping"][id];
console.log(input);
for (let i = 0; i < inputs.length; i++) {
var name;
var value;
switch (inputs[i].tagName) {
case "INPUT":
switch (inputs[i].type) {
case "checkbox":
name = inputs[i].name;
value = inputs[i].checked;
input[name] = value;
break;
case "text":
name = inputs[i].name;
value = inputs[i].value;
input[name] = value;
break;
}
break;
case "SELECT":
name = inputs[i].name;
value = inputs[i].value;
input[name] = value;
break;
}
switch (name) {
case "tvg-logo":
//(document.getElementById(id).childNodes[2].firstChild as HTMLElement).setAttribute("src", value)
break;
case "x-name":
document.getElementById(id).childNodes[3].firstChild.textContent = value;
break;
case "x-category":
document.getElementById(id).childNodes[3].firstChild.className = value;
break;
case "x-group-title":
document.getElementById(id).childNodes[5].firstChild.textContent = value;
break;
case "x-xmltv-file":
if (value != "xTeVe Dummy" && value != "-") {
value = getValueFromProviderFile(value, "xmltv", "name");
}
if (value == "-") {
input["x-active"] = false;
}
document.getElementById(id).childNodes[6].firstChild.textContent = value;
break;
case "x-mapping":
if (value == "-") {
input["x-active"] = false;
}
document.getElementById(id).childNodes[7].firstChild.textContent = value;
break;
default:
}
createSearchObj();
searchInMapping();
}
if (input["x-active"] == false) {
document.getElementById(id).className = "notActiveEPG";
}
else {
document.getElementById(id).className = "activeEPG";
}
console.log(input["tvg-logo"]);
document.getElementById(id).childNodes[2].firstChild.setAttribute("src", input["tvg-logo"]);
});
showElement("popup", false);
return;
}
function showPreview(element) {
var div = document.getElementById("myStreamsBox");
switch (element) {
case false:
div.className = "notVisible";
return;
break;
}
var streams = ["activeStreams", "inactiveStreams"];
streams.forEach(preview => {
var table = document.getElementById(preview);
table.innerHTML = "";
var obj = SERVER["data"]["StreamPreviewUI"][preview];
obj.forEach(channel => {
var tr = document.createElement("TR");
var tdKey = document.createElement("TD");
var tdVal = document.createElement("TD");
tdKey.className = "tdKey";
tdVal.className = "tdVal";
switch (preview) {
case "activeStreams":
tdKey.innerText = "Channel: (+)";
break;
case "inactiveStreams":
tdKey.innerText = "Channel: (-)";
break;
}
tdVal.innerText = channel;
tr.appendChild(tdKey);
tr.appendChild(tdVal);
table.appendChild(tr);
});
});
showElement("loading", false);
div.className = "visible";
return;
}
class XMLTVFile {
getFiles(set) {
var fileIDs = getObjKeys(SERVER["xepg"]["xmltvMap"]);
var values = new Array("-");
var text = new Array("-");
for (let i = 0; i < fileIDs.length; i++) {
if (fileIDs[i] != "xTeVe Dummy") {
values.push(getValueFromProviderFile(fileIDs[i], "xmltv", "file.xteve"));
text.push(getValueFromProviderFile(fileIDs[i], "xmltv", "name"));
}
else {
values.push(fileIDs[i]);
text.push(fileIDs[i]);
}
}
var select = document.createElement("SELECT");
for (let i = 0; i < text.length; i++) {
var option = document.createElement("OPTION");
option.setAttribute("value", values[i]);
option.innerText = text[i];
select.appendChild(option);
}
if (set != "") {
select.value = set;
}
return select;
}
getPrograms(file, set) {
//var fileIDs:string[] = getObjKeys(SERVER["xepg"]["xmltvMap"])
var values = getObjKeys(SERVER["xepg"]["xmltvMap"][file]);
var text = new Array();
var displayName;
for (let i = 0; i < values.length; i++) {
if (SERVER["xepg"]["xmltvMap"][file][values[i]].hasOwnProperty('display-name') == true) {
displayName = SERVER["xepg"]["xmltvMap"][file][values[i]]["display-name"];
}
else {
displayName = "-";
}
text[i] = displayName + " (" + values[i] + ")";
}
text.unshift("-");
values.unshift("-");
var select = document.createElement("SELECT");
for (let i = 0; i < text.length; i++) {
var option = document.createElement("OPTION");
option.setAttribute("value", values[i]);
option.innerText = text[i];
select.appendChild(option);
}
if (set != "") {
select.value = set;
}
if (select.value != set) {
select.value = "-";
}
return select;
}
}
function getValueFromProviderFile(file, fileType, key) {
if (file == "xTeVe Dummy") {
return file;
}
var fileID;
var indicator = file.charAt(0);
switch (indicator) {
case "M":
fileType = "m3u";
fileID = file;
break;
case "H":
fileType = "hdhr";
fileID = file;
break;
case "X":
fileType = "xmltv";
fileID = file.substring(0, file.lastIndexOf('.'));
break;
}
if (SERVER["settings"]["files"][fileType].hasOwnProperty(fileID) == true) {
var data = SERVER["settings"]["files"][fileType][fileID];
return data[key];
}
return;
}
function setXmltvChannel(id, element) {
var xmltv = new XMLTVFile();
var xmlFile = element.value;
var tvgId = SERVER["xepg"]["epgMapping"][id]["tvg-id"];
var td = document.getElementById("popup-mapping").parentElement;
td.innerHTML = "";
var select = xmltv.getPrograms(element.value, tvgId);
select.setAttribute("name", "x-mapping");
select.setAttribute("id", "popup-mapping");
markChangedOnChange(select, function (element) { checkXmltvChannel(id, element, xmlFile); });
select.className = "changed";
sortSelect(select);
td.appendChild(select);
checkXmltvChannel(id, select, xmlFile);
}
function checkXmltvChannel(id, element, xmlFile) {
var value = element.value;
var bool;
var checkbox = document.getElementById('active');
var channel = SERVER["xepg"]["epgMapping"][id];
var updateLogo;
if (value == "-") {
bool = false;
}
else {
bool = true;
}
checkbox.checked = bool;
checkbox.className = "changed";
console.log(xmlFile);
// Kanallogo aktualisieren
/*
updateLogo = (document.getElementById("update-icon") as HTMLInputElement).checked
console.log(updateLogo);
*/
if (xmlFile != "xTeVe Dummy" && bool == true) {
//(document.getElementById("update-icon") as HTMLInputElement).checked = true;
//(document.getElementById("update-icon") as HTMLInputElement).className = "changed";
console.log("ID", id);
changeChannelLogo(id);
return;
}
if (xmlFile == "xTeVe Dummy") {
document.getElementById("update-icon").checked = false;
document.getElementById("update-icon").className = "changed";
}
return;
}
function changeChannelLogo(id) {
var updateLogo;
var channel = SERVER["xepg"]["epgMapping"][id];
var f = document.getElementById("popup-xmltv");
var xmltvFile = f.options[f.selectedIndex].value;
var m = document.getElementById("popup-mapping");
var xMapping = m.options[m.selectedIndex].value;
var xmltvLogo = SERVER["xepg"]["xmltvMap"][xmltvFile][xMapping]["icon"];
updateLogo = document.getElementById("update-icon").checked;
if (updateLogo == true && xmltvFile != "xTeVe Dummy") {
if (SERVER["xepg"]["xmltvMap"][xmltvFile].hasOwnProperty(xMapping)) {
var logo = xmltvLogo;
}
else {
logo = channel["tvg-logo"];
}
var logoInput = document.getElementById("channel-icon");
logoInput.value = logo;
if (BULK_EDIT == false) {
logoInput.className = "changed";
}
}
}
function settingsOptions(text, values) {
var options = [];
for (var i = 0; i < text.length; i++) {
options.push({ text: text[i], value: values[i] });
}
return options;
}
function settingsNumberOptions(from, to) {
var options = [];
for (var i = from; i <= to; i++) {
options.push({ text: i.toString(), value: i.toString() });
}
return options;
}
const SETTINGS_FIELDS = [
// Text inputs
{ key: "update", title: "Schedule for updating (Playlist, XMLTV, Backup)", type: "text", placeholder: "0000,1000,2000",
description: "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated. Leave this field empty if no updates are to be carried out." },
{ key: "backup.path", title: "Location for automatic backups", type: "text", placeholder: "/mnt/data/backup/xteve/",
description: "Before any update of the provider data by the schedule, xTeVe creates a backup. The path for the automatic backups can be changed. xTeVe requires write permission for this folder." },
{ key: "temp.path", title: "Location for the temporary files", type: "text", placeholder: "/tmp/xteve/",
description: "Location for the buffer files." },
{ key: "user.agent", title: "User Agent", type: "text", placeholder: "xTeVe",
description: "User Agent for HTTP requests. For every HTTP connection, this value is used for the user agent. Should only be changed if xTeVe is blocked." },
{ key: "plex.url", title: "Plex Server URL", type: "text", placeholder: "http://plex-host:32400",
description: "Base URL of your Plex server. Example: http://192.168.1.10:32400" },
// The server sends the mask "********" and treats it coming back as "unchanged".
{ key: "plex.token", title: "Plex API Token", type: "password", placeholder: "Plex X-Plex-Token", autocomplete: "off",
description: "Plex token used for authenticated API calls to refresh your DVR guide." },
{ key: "buffer.timeout", title: "Timeout for new client connections", type: "text", placeholder: "100",
description: "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds." },
{ key: "ffmpeg.path", title: "FFmpeg Binary Path", type: "text", placeholder: "/path/to/ffmpeg",
description: "Path to FFmpeg binary." },
{ key: "ffmpeg.options", title: "FFmpeg Options", type: "text", placeholder: "Leave blank to set default settings",
description: "FFmpeg options.
Only change if you know what you are doing.
Leave blank to set default settings." },
{ key: "vlc.path", title: "VLC / CVLC Binary Path", type: "text", placeholder: "/path/to/cvlc",
description: "Path to VLC / CVLC binary." },
{ key: "vlc.options", title: "VLC / CVLC Options", type: "text", placeholder: "Leave blank to set default settings",
description: "VLC / CVLC options.
Only change if you know what you are doing.
Leave blank to set default settings." },
{ key: "udpxy", title: "UDPxy address", type: "text", placeholder: "host:port",
description: "The address of your UDPxy server. If set, and the channel URLs in the m3u is multicast, xTeVe will rewrite it so that it is accessed via the UDPxy service." },
// Checkboxes
{ key: "authentication.web", title: "WEB Authentication", type: "checkbox",
description: "Access to the web interface only possible with credentials." },
{ key: "authentication.pms", title: "PMS Authentication", type: "checkbox",
description: "Plex requests are only possible with authentication.
Warning!!! After activating this function xTeVe must be delete in the PMS DVR settings and set up again." },
{ key: "authentication.m3u", title: "M3U Authentication", type: "checkbox",
description: "Downloading the xteve.m3u file via an HTTP request is only possible with authentication." },
{ key: "authentication.xml", title: "XML Authentication", type: "checkbox",
description: "Downloading the xteve.xml file via an HTTP request is only possible with authentication" },
// Only rendered while web authentication is on (see SettingsCategoryItem.createCategory).
{ key: "authentication.api", title: "API Authentication", type: "checkbox",
description: "Access to the API interface is only possible with authentication." },
{ key: "files.update", title: "Updates all files at startup", type: "checkbox",
description: "Updates all playlists, tuner and XMLTV files at startup." },
{ key: "cache.images", title: "Image Caching", type: "checkbox",
description: "All images from the XMLTV file are cached, allowing faster rendering of the grid in the client.
Downloading the images may take a while and will be done in the background." },
{ key: "xepg.replace.missing.images", title: "Replace missing program images", type: "checkbox",
description: "If the poster in the XMLTV program is missing, the channel logo will be used." },
{ key: "api", title: "API Interface", type: "checkbox",
description: "Via API interface it is possible to send commands to xTeVe. API documentation is here" },
{ key: "use_plexAPI", title: "Use Plex API Refresh", type: "checkbox",
description: "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates." },
// Selects
{ key: "xepg.missing.epg.mode", title: "Missing EPG Handling", type: "select",
options: settingsOptions(["Strict (deactivate channels)", "Relaxed (keep active / dummy guide)"], ["strict", "relaxed"]),
description: "Strict: channels are deactivated when XMLTV mappings disappear.
Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved." },
{ key: "tuner", title: "Number of Tuners", type: "select",
options: settingsNumberOptions(1, 100),
description: "Number of parallel connections that can be established to the provider.
Available for: Plex, Emby (HDHR), M3U (with active buffer).
After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again." },
{ key: "epgSource", title: "EPG Source", type: "select",
options: settingsOptions(["PMS", "XEPG"], ["PMS", "XEPG"]),
description: "PMS:
- Use EPG data from Plex or Emby
XEPG:
- Use of one or more XMLTV files
- Channel management
- M3U / XMLTV export (HTTP link for IPTV apps)" },
{ key: "backup.keep", title: "Number of backups to keep", type: "select",
options: settingsOptions(["5", "10", "20", "30", "40", "50"], ["5", "10", "20", "30", "40", "50"]),
description: "Number of backups to keep. Older backups are automatically deleted." },
{ key: "buffer.size.kb", title: "Buffer Size", type: "select",
options: settingsOptions(["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"], ["512", "1024", "2048", "3072", "4096", "5120", "6144", "7168", "8192"]),
description: "Buffer size in MB.
M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used." },
{ key: "buffer", title: "Stream Buffer", type: "select",
options: settingsOptions(["No Buffer (Client connects to the streaming server)", "xTeVe: (xTeVe connects to the streaming server)", "FFmpeg: (FFmpeg connects to the streaming server)", "VLC: (VLC connects to the streaming server)"], ["-", "xteve", "ffmpeg", "vlc"]),
description: "Functions of the buffer:
- The stream is passed from xTeVe, FFmpeg or VLC to Plex, Emby or M3U Player
- Small jerking of the streams can be compensated
- HLS / M3U8 support
- RTP / RTPS support (only FFmpeg or VLC)
- Re-streaming
- Separate tuner limit for each playlist" },
];
function getSettingsField(settingsKey) {
for (var i = 0; i < SETTINGS_FIELDS.length; i++) {
if (SETTINGS_FIELDS[i].key == settingsKey) {
return SETTINGS_FIELDS[i];
}
}
return undefined;
}
class SettingsCategory {
constructor() {
this.DocumentID = "content_settings";
}
createCategoryHeadline(value) {
var element = document.createElement("H4");
element.innerHTML = value;
return element;
}
createHR() {
var element = document.createElement("HR");
return element;
}
// createControl builds the input for one field, filled with the current value.
createControl(field, data) {
var content = new PopupContent();
var control;
switch (field.type) {
case "checkbox":
control = content.createCheckbox(field.key);
control.checked = data;
break;
case "select":
var text = field.options.map(function (option) { return option.text; });
var values = field.options.map(function (option) { return option.value; });
control = content.createSelect(text, values, data, field.key);
break;
default:
// "text", "password", "number": lists (the update schedule) render comma separated.
var value = data;
if (Array.isArray(data)) {
value = data.join(",");
}
control = content.createInput(field.type, field.key, value);
if (field.placeholder != undefined) {
control.setAttribute("placeholder", field.placeholder);
}
if (field.autocomplete != undefined) {
control.setAttribute("autocomplete", field.autocomplete);
}
break;
}
markChangedOnChange(control);
return control;
}
createSettings(settingsKey) {
var setting = document.createElement("TR");
var field = getSettingsField(settingsKey);
if (field == undefined) {
return setting;
}
var data = SERVER["settings"][settingsKey];
var tdLeft = document.createElement("TD");
tdLeft.textContent = field.title + ":";
var tdRight = document.createElement("TD");
tdRight.appendChild(this.createControl(field, data));
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
return setting;
}
createDescription(settingsKey) {
var description = document.createElement("TR");
var field = getSettingsField(settingsKey);
var text = "";
if (field != undefined) {
text = field.description;
}
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "";
var tdRight = document.createElement("TD");
var pre = document.createElement("PRE");
pre.innerHTML = text;
tdRight.appendChild(pre);
description.appendChild(tdLeft);
description.appendChild(tdRight);
return description;
}
}
class SettingsCategoryItem extends SettingsCategory {
constructor(headline, settingsKeys) {
super();
this.headline = headline;
this.settingsKeys = settingsKeys;
}
createCategory() {
var headline = this.createCategoryHeadline(this.headline);
var settingsKeys = this.settingsKeys;
var doc = document.getElementById(this.DocumentID);
doc.appendChild(headline);
// Tabelle für die Kategorie erstellen
var table = document.createElement("TABLE");
var keys = settingsKeys.split(",");
keys.forEach(settingsKey => {
switch (settingsKey) {
case "authentication.pms":
case "authentication.m3u":
case "authentication.xml":
case "authentication.api":
if (SERVER["settings"]["authentication.web"] == false) {
break;
}
default:
var item = this.createSettings(settingsKey);
var description = this.createDescription(settingsKey);
table.appendChild(item);
table.appendChild(description);
break;
}
});
doc.appendChild(table);
doc.appendChild(this.createHR());
}
}
function showSettings() {
console.log("SETTINGS");
for (let i = 0; i < settingsCategory.length; i++) {
settingsCategory[i].createCategory();
}
}
function saveSettings() {
console.log("Save Settings");
var cmd = "saveSettings";
var div = document.getElementById("content_settings");
var settings = div.getElementsByClassName("changed");
var newSettings = new Object();
for (let i = 0; i < settings.length; i++) {
var name;
var value;
switch (settings[i].tagName) {
case "INPUT":
switch (settings[i].type) {
case "checkbox":
name = settings[i].name;
value = settings[i].checked;
newSettings[name] = value;
break;
case "text":
case "password":
name = settings[i].name;
value = settings[i].value;
switch (name) {
case "update":
value = value.split(",");
value = value.filter(function (e) { return e; });
break;
case "buffer.timeout":
value = parseFloat(value);
}
newSettings[name] = value;
break;
}
break;
case "SELECT":
name = settings[i].name;
value = settings[i].value;
// Wenn der Wert eine Zahl ist, wird dieser als Zahl gespeichert
if (isNaN(value)) {
newSettings[name] = value;
}
else {
newSettings[name] = parseInt(value);
}
break;
}
}
var data = new Object();
data["settings"] = newSettings;
var server = new Server(cmd);
server.request(data);
}
class Log {
createLog(entry) {
var element = document.createElement("PRE");
if (entry.indexOf("WARNING") != -1) {
element.className = "warningMsg";
}
if (entry.indexOf("ERROR") != -1) {
element.className = "errorMsg";
}
if (entry.indexOf("DEBUG") != -1) {
element.className = "debugMsg";
}
element.textContent = entry;
return element;
}
}
function showLogs(bottom) {
var log = new Log();
var logs = SERVER["log"]["log"];
var div = document.getElementById("content_log");
var wrapper = document.getElementById("box-wrapper");
var shouldStickToBottom = bottom;
div.innerHTML = "";
if (wrapper != null && shouldStickToBottom == false) {
var distanceToBottom = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight;
if (distanceToBottom < 80) {
shouldStickToBottom = true;
}
}
var keys = getObjKeys(logs);
keys.forEach(logID => {
var entry = log.createLog(logs[logID]);
div.append(entry);
});
setTimeout(function () {
if (shouldStickToBottom == true && wrapper != null) {
wrapper.scrollTop = wrapper.scrollHeight;
}
}, 10);
}
function resetLogs() {
var cmd = "resetLogs";
var data = new Object();
var server = new Server(cmd);
server.request(data);
}
var SERVER = new Object();
var BULK_EDIT = false;
var COLUMN_TO_SORT;
var SEARCH_MAPPING = new Object();
var UNDO = new Object();
var WS_AVAILABLE = false;
var ACTIVE_MENU_ID = "";
var LAST_BULK_CHECKBOX = null;
// Menü
var menuItems = new Array();
menuItems.push(new MainMenuItem("playlist", "Playlist", "m3u.png", "Local or remote playlists"));
//menuItems.push(new MainMenuItem("pmsID", "PMS ID", "number.png", ""))
menuItems.push(new MainMenuItem("filter", "Filter", "filter.png", "Filter playlist"));
menuItems.push(new MainMenuItem("xmltv", "XMLTV", "xmltv.png", "Local or remote XMLTV files"));
menuItems.push(new MainMenuItem("mapping", "Mapping", "mapping.png", "Map playlist channels to EPG channels"));
menuItems.push(new MainMenuItem("users", "Users", "users.png", "User management"));
menuItems.push(new MainMenuItem("settings", "Settings", "settings.png", "Settings"));
menuItems.push(new MainMenuItem("log", "Log", "log.png", "Log"));
menuItems.push(new MainMenuItem("logout", "Logout", "logout.png", "Logout"));
// Kategorien für die Einstellungen
var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("General", "tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
settingsCategory.push(new SettingsCategoryItem("Files", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("Streaming", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
settingsCategory.push(new SettingsCategoryItem("Backup", "backup.path,backup.keep"));
settingsCategory.push(new SettingsCategoryItem("Authentication", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"));
function showPopUpElement(elm) {
var allElements = new Array("popup-custom");
for (var i = 0; i < allElements.length; i++) {
showElement(allElements[i], false);
}
showElement(elm, true);
setTimeout(function () {
showElement("popup", true);
}, 10);
return;
}
function showElement(elmID, type) {
var cssClass;
switch (type) {
case true:
cssClass = "block";
break;
case false:
cssClass = "none";
break;
}
var element = document.getElementById(elmID);
if (element == null) {
return;
}
element.className = cssClass;
}
// announce writes a message into the ARIA live region so assistive
// technology hears it; the region is created when the page has none.
function announce(message) {
if (document.body == null) {
return;
}
var region = document.getElementById("announcer");
if (region == null) {
region = document.getElementById("sr-announcer");
}
if (region == null) {
region = document.createElement("DIV");
region.id = "announcer";
region.className = "sr-only";
region.setAttribute("aria-live", "polite");
region.setAttribute("aria-atomic", "true");
document.body.appendChild(region);
}
region.textContent = message;
}
// alertUser shows the browser alert and mirrors it into the live region.
function alertUser(message) {
announce(message);
alert(message);
}
function setConnectionState(state, text = "") {
var label = text;
if (label == undefined || label.length == 0) {
switch (state) {
case "online":
label = "Connected";
break;
case "busy":
label = "Syncing";
break;
case "offline":
label = "Offline";
break;
default:
label = "Connecting";
break;
}
}
var indicator = document.getElementById("connection-indicator");
if (indicator == null) {
return;
}
indicator.className = "status-" + state;
indicator.innerText = label;
}
// markChangedOnChange flags a control as edited on "change" so that
// saveSettings / donePopupData pick it up, then runs an optional follow-up.
// Callers build many controls through one reused `var`, so the listener must
// close over this parameter and not over the caller's variable.
function markChangedOnChange(element, then) {
element.addEventListener("change", function () {
element.className = "changed";
if (then != undefined) {
then(element);
}
});
}
function getLocalData(dataType, id) {
var data = new Object();
switch (dataType) {
case "m3u":
data = SERVER["settings"]["files"][dataType][id];
break;
case "hdhr":
data = SERVER["settings"]["files"][dataType][id];
break;
case "filter":
case "custom-filter":
case "group-title":
if (id == -1) {
data["active"] = true;
data["caseSensitive"] = false;
data["description"] = "";
data["exclude"] = "";
data["filter"] = "";
data["include"] = "";
data["name"] = "";
data["type"] = "group-title";
SERVER["settings"]["filter"][id] = data;
}
data = SERVER["settings"]["filter"][id];
break;
case "xmltv":
data = SERVER["settings"]["files"][dataType][id];
break;
case "users":
data = SERVER["users"][id]["data"];
break;
case "mapping":
data = SERVER["xepg"]["epgMapping"][id];
break;
case "m3uGroups":
data = SERVER["data"]["playlist"]["m3u"]["groups"];
break;
}
return data;
}
function getObjKeys(obj) {
var keys = new Array();
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
keys.push(i);
}
}
return keys;
}
function getAllSelectedChannels() {
var channels = new Array();
if (BULK_EDIT == false) {
return channels;
}
var trs = document.getElementById("content_table").getElementsByTagName("TR");
for (var i = 1; i < trs.length; i++) {
if (trs[i].style.display != "none") {
if (trs[i].firstChild.firstChild.checked == true) {
channels.push(trs[i].id);
}
}
}
return channels;
}
function scheduleChannelRangeSelection(checkbox, event) {
var shiftPressed = false;
if (event != undefined && event.shiftKey == true) {
shiftPressed = true;
}
// Run after the native checkbox toggle so we copy the final checked state.
setTimeout(function () {
selectChannelRange(checkbox, shiftPressed);
}, 0);
}
function selectChannelRange(checkbox, shiftPressed) {
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
return;
}
var table = document.getElementById("content_table");
if (table == null) {
return;
}
var trs = table.getElementsByTagName("TR");
var visibleCheckboxes = new Array();
for (var i = 1; i < trs.length; i++) {
if (trs[i].style.display != "none") {
var bulkCheckbox = trs[i].querySelector("input.bulk");
if (bulkCheckbox != null) {
visibleCheckboxes.push(bulkCheckbox);
}
}
}
var currentIndex = visibleCheckboxes.indexOf(checkbox);
var previousIndex = -1;
if (LAST_BULK_CHECKBOX != null) {
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX);
}
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
var start = Math.min(previousIndex, currentIndex);
var end = Math.max(previousIndex, currentIndex);
for (var i = start; i <= end; i++) {
visibleCheckboxes[i].checked = checkbox.checked;
}
}
LAST_BULK_CHECKBOX = checkbox;
}
function selectAllChannels() {
var bulk = false;
var trs = document.getElementById("content_table").getElementsByTagName("TR");
if (trs[0].firstChild.firstChild.checked == true) {
bulk = true;
}
for (var i = 1; i < trs.length; i++) {
if (trs[i].style.display != "none") {
switch (bulk) {
case true:
trs[i].firstChild.firstChild.checked = true;
break;
case false:
trs[i].firstChild.firstChild.checked = false;
break;
}
}
}
LAST_BULK_CHECKBOX = null;
return;
}
function bulkEdit() {
BULK_EDIT = !BULK_EDIT;
var className;
var rows = document.getElementsByClassName("bulk");
switch (BULK_EDIT) {
case true:
className = "bulk showBulk";
break;
case false:
className = "bulk hideBulk";
break;
}
for (var i = 0; i < rows.length; i++) {
rows[i].className = className;
rows[i].checked = false;
}
LAST_BULK_CHECKBOX = null;
return;
}
function sortTable(column) {
//console.log(columm);
if (column == COLUMN_TO_SORT) {
return;
}
var table = document.getElementById("content_table");
var tableHead = table.getElementsByTagName("TR")[0];
var tableItems = tableHead.getElementsByTagName("TD");
var sortObj = new Object();
var x, xValue;
var tableHeader;
var sortByString = false;
if (column > 0 && COLUMN_TO_SORT > 0) {
tableItems[COLUMN_TO_SORT].className = "pointer";
tableItems[column].className = "sortThis";
}
COLUMN_TO_SORT = column;
var rows = table.rows;
if (rows[1] != undefined) {
tableHeader = rows[0];
x = rows[1].getElementsByTagName("TD")[column];
for (i = 1; i < rows.length; i++) {
x = rows[i].getElementsByTagName("TD")[column];
switch (x.childNodes[0].tagName.toLowerCase()) {
case "input":
xValue = x.getElementsByTagName("INPUT")[0].value.toLowerCase();
break;
case "p":
xValue = x.getElementsByTagName("P")[0].innerText.toLowerCase();
break;
default: console.log(x.childNodes[0].tagName);
}
if (xValue == "" || Number.isNaN(xValue)) {
xValue = i;
sortObj[i] = rows[i];
}
else {
switch (isNaN(xValue)) {
case false:
xValue = parseFloat(xValue);
sortObj[xValue] = rows[i];
break;
case true:
sortByString = true;
sortObj[xValue.toLowerCase() + i] = rows[i];
break;
}
}
}
while (table.firstChild) {
table.removeChild(table.firstChild);
}
var sortValues = getObjKeys(sortObj);
if (sortByString == true) {
sortValues.sort();
console.log(sortValues);
}
else {
function sortFloat(a, b) {
return a - b;
}
sortValues.sort(sortFloat);
}
table.appendChild(tableHeader);
for (var i = 0; i < sortValues.length; i++) {
table.appendChild(sortObj[sortValues[i]]);
}
}
return;
}
function createSearchObj() {
SEARCH_MAPPING = new Object();
var data = SERVER["xepg"]["epgMapping"];
var channels = getObjKeys(data);
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"];
channels.forEach(id => {
channelKeys.forEach(key => {
if (key == "x-active") {
switch (data[id][key]) {
case true:
SEARCH_MAPPING[id] = "online ";
break;
case false:
SEARCH_MAPPING[id] = "offline ";
break;
}
}
else {
if (key == "x-xmltv-file") {
var xmltvFile = getValueFromProviderFile(data[id][key], "xmltv", "name");
if (xmltvFile != undefined) {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + xmltvFile + " ";
}
}
else {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " ";
}
}
});
});
return;
}
function searchInMapping() {
var searchValue = document.getElementById("searchMapping").value;
var trs = document.getElementById("content_table").getElementsByTagName("TR");
for (var i = 1; i < trs.length; ++i) {
var id = trs[i].getAttribute("id");
var element = SEARCH_MAPPING[id];
switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
case true:
document.getElementById(id).style.display = "";
break;
case false:
document.getElementById(id).style.display = "none";
break;
}
}
return;
}
function calculateWrapperHeight() {
var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
if (elm != null && content != null) {
var contentTop = content.getBoundingClientRect().top;
var freeSpace = window.innerHeight - contentTop - 26;
if (freeSpace < 180) {
freeSpace = 180;
}
elm.style.height = freeSpace + "px";
}
return;
}
function changeChannelNumber(element) {
var dbID = element.parentNode.parentNode.id;
var newNumber = parseFloat(element.value);
var channelNumbers = [];
var data = SERVER["xepg"]["epgMapping"];
var channels = getObjKeys(data);
if (isNaN(newNumber)) {
alertUser("Invalid channel number");
return;
}
channels.forEach(id => {
var channelNumber = parseFloat(data[id]["x-channelID"]);
channelNumbers.push(channelNumber);
});
for (var i = 0; i < channelNumbers.length; i++) {
if (channelNumbers.indexOf(newNumber) == -1) {
break;
}
if (Math.floor(newNumber) == newNumber) {
newNumber = newNumber + 1;
}
else {
newNumber = newNumber + 0.1;
newNumber.toFixed(1);
newNumber = Math.round(newNumber * 10) / 10;
}
}
data[dbID]["x-channelID"] = newNumber.toString();
element.value = newNumber;
console.log(data[dbID]["x-channelID"]);
if (COLUMN_TO_SORT == 1) {
COLUMN_TO_SORT = -1;
sortTable(1);
}
return;
}
function backup() {
var data = new Object();
console.log("Backup data");
var cmd = "xteveBackup";
console.log("SEND TO SERVER");
console.log(data);
var server = new Server(cmd);
server.request(data);
return;
}
function toggleChannelStatus(id) {
var element;
var status;
if (document.getElementById("active")) {
var checkbox = document.getElementById("active");
status = (checkbox).checked;
}
var ids = getAllSelectedChannels();
if (ids.length == 0) {
ids.push(id);
}
ids.forEach(id => {
var channel = SERVER["xepg"]["epgMapping"][id];
channel["x-active"] = status;
switch (channel["x-active"]) {
case true:
if (channel["x-xmltv-file"] == "-" || channel["x-mapping"] == "-") {
if (BULK_EDIT == false) {
alertUser(channel["x-name"] + ": Missing XMLTV file / channel");
checkbox.checked = false;
}
channel["x-active"] = false;
}
break;
case false:
// code...
break;
}
if (channel["x-active"] == false) {
document.getElementById(id).className = "notActiveEPG";
}
else {
document.getElementById(id).className = "activeEPG";
}
});
}
function restore() {
if (document.getElementById('upload')) {
document.getElementById('upload').remove();
}
var restore = document.createElement("INPUT");
restore.setAttribute("type", "file");
restore.setAttribute("class", "notVisible");
restore.setAttribute("name", "");
restore.id = "upload";
document.body.appendChild(restore);
restore.click();
restore.onchange = function () {
var filename = restore.files[0].name;
var check = confirm("File: " + filename + "\nAll data will be replaced with those from the backup. Should the files be restored?");
if (check == true) {
var reader = new FileReader();
var file = document.querySelector('input[type=file]').files[0];
if (file) {
reader.readAsDataURL(file);
reader.onload = function () {
console.log(reader.result);
var data = new Object();
var cmd = "xteveRestore";
data["base64"] = reader.result;
var server = new Server(cmd);
server.request(data);
};
}
else {
alertUser("File could not be loaded");
}
restore.remove();
return;
}
};
return;
}
function uploadLogo() {
if (document.getElementById('upload')) {
document.getElementById('upload').remove();
}
var upload = document.createElement("INPUT");
upload.setAttribute("type", "file");
upload.setAttribute("class", "notVisible");
upload.setAttribute("name", "");
upload.id = "upload";
document.body.appendChild(upload);
upload.click();
upload.onblur = function () {
alert();
};
upload.onchange = function () {
var filename = upload.files[0].name;
var reader = new FileReader();
var file = document.querySelector('input[type=file]').files[0];
if (file) {
reader.readAsDataURL(file);
reader.onload = function () {
console.log(reader.result);
var data = new Object();
var cmd = "uploadLogo";
data["base64"] = reader.result;
data["filename"] = file.name;
var server = new Server(cmd);
server.request(data);
var updateLogo = document.getElementById('update-icon');
updateLogo.checked = false;
updateLogo.className = "changed";
};
}
else {
alertUser("File could not be loaded");
}
upload.remove();
return;
};
}
function checkUndo(key) {
switch (key) {
case "epgMapping":
if (UNDO.hasOwnProperty(key)) {
SERVER["xepg"][key] = JSON.parse(JSON.stringify(UNDO[key]));
}
else {
UNDO[key] = JSON.parse(JSON.stringify(SERVER["xepg"][key]));
}
break;
default:
break;
}
return;
}
function sortSelect(elem) {
var tmpAry = [];
var selectedValue = elem[elem.selectedIndex].value;
for (var i = 0; i < elem.options.length; i++)
tmpAry.push(elem.options[i]);
tmpAry.sort(function (a, b) { return (a.text < b.text) ? -1 : 1; });
while (elem.options.length > 0)
elem.options[0] = null;
var newSelectedIndex = 0;
for (var i = 0; i < tmpAry.length; i++) {
elem.options[i] = tmpAry[i];
if (elem.options[i].value == selectedValue)
newSelectedIndex = i;
}
elem.selectedIndex = newSelectedIndex; // Set new selected index after sorting
return;
}
function updateLog() {
var server = new Server("updateLog");
server.request(new Object());
}
class WizardCategory {
constructor() {
this.DocumentID = "content";
}
createCategoryHeadline(value) {
var element = document.createElement("H4");
element.innerHTML = value;
return element;
}
}
class WizardItem extends WizardCategory {
constructor(key, headline) {
super();
this.headline = headline;
this.key = key;
}
createWizard() {
var headline = this.createCategoryHeadline(this.headline);
var key = this.key;
var content = new PopupContent();
var description;
var doc = document.getElementById(this.DocumentID);
doc.innerHTML = "";
doc.appendChild(headline);
switch (key) {
case "tuner":
var text = new Array();
var values = new Array();
for (var i = 1; i <= 100; i++) {
text.push(i);
values.push(i);
}
var select = content.createSelect(text, values, "1", key);
select.setAttribute("class", "wizard");
select.id = key;
doc.appendChild(select);
description = "Number of parallel connections that can be established to the provider.
Available for: Plex, Emby (HDHR), M3U (with active buffer).
After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again.";
break;
case "epgSource":
var text = ["PMS", "XEPG"];
var values = ["PMS", "XEPG"];
var select = content.createSelect(text, values, "XEPG", key);
select.setAttribute("class", "wizard");
select.id = key;
doc.appendChild(select);
description = "PMS:
- Use EPG data from Plex or Emby
XEPG:
- Use of one or more XMLTV files
- Channel management
- M3U / XMLTV export (HTTP link for IPTV apps)";
break;
case "m3u":
var input = content.createInput("text", key, "");
input.setAttribute("placeholder", "File path or URL of the M3U");
input.setAttribute("class", "wizard");
input.id = key;
doc.appendChild(input);
description = "Local or remote playlists";
break;
case "xmltv":
var input = content.createInput("text", key, "");
input.setAttribute("placeholder", "File path or URL of the XMLTV");
input.setAttribute("class", "wizard");
input.id = key;
doc.appendChild(input);
description = "Local or remote XMLTV file";
break;
default:
console.log(key);
break;
}
var pre = document.createElement("PRE");
pre.innerHTML = description;
doc.appendChild(pre);
console.log(headline, key);
}
}
function readyForConfiguration(wizard) {
var server = new Server("getServerConfig");
server.request(new Object());
showElement("loading", false);
configurationWizard[wizard].createWizard();
}
function saveWizard() {
var cmd = "saveWizard";
var div = document.getElementById("content");
var config = div.getElementsByClassName("wizard");
var wizard = new Object();
for (var i = 0; i < config.length; i++) {
var name;
var value;
switch (config[i].tagName) {
case "SELECT":
name = config[i].name;
value = config[i].value;
// Wenn der Wert eine Zahl ist, wird dieser als Zahl gespeichert
if (isNaN(value)) {
wizard[name] = value;
}
else {
wizard[name] = parseInt(value);
}
break;
case "INPUT":
switch (config[i].type) {
case "text":
name = config[i].name;
value = config[i].value;
if (value.length == 0) {
var msg = name.toUpperCase() + ": " + "Missing input";
alertUser(msg);
return;
}
wizard[name] = value;
break;
}
break;
default:
// code...
break;
}
}
var data = new Object();
data["wizard"] = wizard;
var server = new Server(cmd);
server.request(data);
console.log(data);
}
// Wizard
var configurationWizard = new Array();
configurationWizard.push(new WizardItem("tuner", "Number of tuners"));
configurationWizard.push(new WizardItem("epgSource", "EPG Source"));
configurationWizard.push(new WizardItem("m3u", "M3U Playlist"));
configurationWizard.push(new WizardItem("xmltv", "XMLTV File"));
function login() {
var err = false;
var data = new Object();
var div = document.getElementById("content");
var form = document.getElementById("authentication");
var inputs = div.getElementsByTagName("INPUT");
console.log(inputs);
for (var i = inputs.length - 1; i >= 0; i--) {
var key = inputs[i].name;
var value = inputs[i].value;
if (value.length == 0) {
inputs[i].style.borderColor = "red";
err = true;
}
else {
inputs[i].style.borderColor = "";
}
data[key] = value;
}
if (err == true) {
data = new Object();
return;
}
if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) {
document.getElementById('password').style.borderColor = "red";
document.getElementById('confirm').style.borderColor = "red";
document.getElementById("err").innerHTML = "Password does not match";
return;
}
}
console.log(data);
form.submit();
}