v2.0.0.0000

This commit is contained in:
marmei
2019-08-02 20:12:09 +02:00
parent e396af91cb
commit e001b06b62
85 changed files with 22786 additions and 2 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.DS_Store
compiler
files
update_xteve*.sh
xteve
xteve.exe
de.json

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 xteve-project
Copyright (c) 2019 marmei@xteve-project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

1
README-DEV.md Normal file
View File

@@ -0,0 +1 @@
# Information for the developers will come soon

128
README.md
View File

@@ -1 +1,127 @@
# xTeVe
<div align="center" style="background-color: #111; padding: 100;">
<a href="https://github.com/xteve-project/xTeVe"><img width="600" height="200" src="html/img/logo_w_600x200.png" alt="xTeVe" /></a>
</div>
<br>
# xTeVe
## M3U Proxy for Plex DVR and Emby Live TV.
Documentation for setup and configuration is [here](https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md).
#### Donation
* **Bitcoin:** 1c1iCe4CJPfNUXtqxKBbW2Qd2EtqRPWme
![Bitcoin](html/img/BC-QR.jpg "Bitcoin - xTeVe")
## Requirements
### Plex
* Plex Media Server (1.11.1.4730 or newer)
* Plex Client with DVR support
* Plex Pass
### Emby
* Emby Server (3.5.3.0 or newer)
* Emby Client with Live-TV support
* Emby Premiere
---
## Features
#### Files
* Merge external M3U files
* Merge external XMLTV files
* Automatic M3U and XMLTV update
* M3U und XMLTV export
#### Channel management
* Filtering streams
* Channel mapping
* Channel order
* Channel logos
* Channel categories
#### Streaming
* Buffer with HLS / M3U8 support
* Re-streaming
* Number of tuners adjustable
* Compatible with Plex / Emby EPG
---
## Downloads v2 | 64 Bit only
#### 64 Bit Intel / AMD
* [Windows](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_windows_amd64.zip?raw=true)
* [OS X](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_darwin_amd64.zip?raw=true)
* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_amd64.zip?raw=true)
* [FreeBSD](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_freebsd_amd64.zip?raw=true)
#### 64 Bit ARM
* [Linux](https://github.com/xteve-project/xTeVe-Downloads/blob/master/xteve_linux_arm64.zip?raw=true)
#### Recommended Docker Image (Linux 64 Bit)
Thanks to @alturismo and @LeeD for creating the Docker Images.
**Created by alturismo:**
[xTeVe](https://hub.docker.com/r/alturismo/xteve)
[xTeVe / Guide2go](https://hub.docker.com/r/alturismo/xteve_guide2go)
Including:
- Guide2go: XMLTV grabber for Schedules Direct
**Created by LeeD:**
[xTeVe / Guide2go / Zap2XML](https://hub.docker.com/r/dnsforge/xteve)
Including:
- Guide2go: XMLTV grabber for Schedules Direct
- Zap2XML: Perl based zap2it XMLTV grabber
- Bash: A Unix / Linux shell
- Crond: Daemon to execute scheduled commands
- Perl: Programming language
---
## Build from source code [Go / Golang]
#### Requirements
* Go (go1.12.4 or newer)
#### Dependancys
* [go-ssdp](https://github.com/koron/go-ssdp)
* [websocket](https://github.com/gorilla/websocket)
* [osext](https://github.com/kardianos/osext)
#### Build
1. Download source code
2. Install dependancys
```
go get github.com/koron/go-ssdp
go get github.com/gorilla/websocket
go get github.com/kardianos/osext
```
3. Build xTeVe
```
go build xteve.go
```
---
## Fork without pull request :mega:
When creating a fork, the xTeVe GitHub account must be changed from the sorce code or the update function disabled.
Future updates of the xteve-project would update your fork. :wink:
xteve.go - Line: 29
```Go
var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-Downloads", Update: true}
/*
Branch: GitHub Branch
User: GitHub Username
Repo: GitHub Repository
Update: Automatic updates from the GitHub repository [true|false]
*/
```

58
html/configuration.html Normal file
View File

@@ -0,0 +1,58 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css">
<script language="javascript" type="text/javascript" src="js/configuration_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/menu_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/settings_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
</head>
<body onload="javascript: readyForConfiguration(0);">
<div id="loading" class="block">
<div class="loader"></div>
</div>
<div id="header" class="imgCenter"></div>
<div id="box">
<table id="clientInfo" class="visible">
<tr>
<td class="tdKey">Version:</td>
<td id="version" class="tdVal">&nbsp;</td>
<td class="tdKey">OS:</td>
<td id="os" class="tdVal">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">UUID:</td>
<td id="uuid" class="tdVal">&nbsp;</td>
<td class="tdKey">Arch:</td>
<td id="arch" class="tdVal">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">Streams:</td>
<td id="streams" class="tdVal">&nbsp;</td>
<td class="tdKey">DVR:</td>
<td id="DVR" class="tdVal">&nbsp;</td>
</tr>
</table>
<div id="headline">
<h1 id="head-text" class="center">Configuration</h1>
</div>
<p id="err" class="errorMsg center"></p>
<div id="content">
</div>
<div id="box-footer">
<input id="next" class="" type="button" name="next" value="Next" onclick="javascript: saveWizard();">
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,47 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css">
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head>
<body>
<div id="header" class="imgCenter"></div>
<div id="box">
<div id="headline">
<h1 id="head-text" class="center">{{.account.headline}}</h1>
</div>
<p id="err" class="errorMsg center"></p>
<div id="content">
<form id="authentication" action="/web/" method="post">
<h5>{{.account.username.title}}:</h5>
<input id="username" type="text" name="username" placeholder="Username" value="">
<h5>{{.account.password.title}}:</h5>
<input id="password" type="password" name="password" placeholder="Password" value="">
<h5>{{.account.confirm.title}}:</h5>
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value="">
</form>
</div>
<div id="box-footer">
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" onclick="javascript: login();">
</div>
</div>
</body>
</html>

448
html/css/base.css Normal file
View File

@@ -0,0 +1,448 @@
* {
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
font-family: "Arial", sans-serif;
letter-spacing: 2px;
}
/*
::-webkit-scrollbar {
display: none;
}
*/
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 5px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0,0.6);
background-color: #444;
}
::-webkit-scrollbar-thumb:hover {
background: #333;
}
::-webkit-scrollbar-corner {
background: transparent;
}
a {
color: #00E6FF;
}
html, body {
color: #fff;
margin: 0px auto;
height: 100%;
font-size: 14px;
}
h2 {
font-size: 24px;
letter-spacing: 2px;
}
h3 {
font-size: 22px;
letter-spacing: 1px;
}
h4 {
font-size: 20px;
letter-spacing: 1px;
line-height: 1.5em;
}
h5 {
font-size: 16px;
letter-spacing: 1px;
line-height: 1.2em;
margin: 25px 0px 10px 0px;
}
hr {
border: 0;
height: 1px;
background: #333;
margin: 10px 0px;
}
p {
margin: 2px;
padding: 2px 5px;
}
pre {
margin: 0px 0px 5px 0px;
font-size: 12px;
color: #ddd;
letter-spacing: 1px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
font-style: normal;
font-variant: normal;
line-height: 1.6em;
}
label {
margin-bottom: 20px;
display: block;
}
li {
list-style-type: none;
background-color: #111;
padding: 10px 20px;
cursor: pointer;
border-left: solid 2px #111;
transition: all 0.3;
}
li:hover {
border-color: #00E6FF
}
select {
cursor: pointer;
width: calc(100% + 2px);
border: solid 0px #00E6FF;
border-radius: 0px;
outline: none;
color: #fff;
padding: 9px 10px;
display:block;
background-color: #333;
font-size: 14px;
margin: 5px 0px 5px 0px;
}
select:focus {
outline: none;
}
input {
-webkit-appearance: none;
margin: 5px 0px;
padding: 2.5px 10px;
outline: none;
font-size: 14px;
}
input[type=button], input[type=submit] {
cursor: pointer;
background-color: #000;
margin: 10px 10px;
padding: 10px 25px;
border: solid 0px;
border-color: #000;
border-radius: 3px;
outline: none;
color: #fff;
}
input[type=button]:focus {
outline: none;
}
input[type=button]:hover {
background-color: #00E6FF;
color: #000;
}
input[type=button]:hover.delete {
background-color: red;
color: #fff;
}
input[type=text], input[type=search], input[type=password] {
color: #fff;
width: -webkit-calc(100% - 0px);
width: -moz-calc(100% - 0px);
width: calc(100% - 0px);
outline: none;
border: solid 1px transparent;
background-color: transparent;
border-bottom-color: #555;
border-radius: 0px;
padding: 8px 10px;
}
input[type="checkbox"] {
border: solid 1px #00E6FF;
background-color: #333;
height: 25px;
width: 25px;
cursor: pointer;
/*
-webkit-appearance: checkbox;
*/
}
input[type="checkbox"]:checked {
color: #fff;
background-color: #00E6FF;
/*display: inline-block;*/
}
input[type="checkbox"]:before {
position: initial;
left: 0px;
margin-left: -4px;
content: " ";
}
input[type="checkbox"]:checked:before {
position: initial;
left: 0px;
margin-left: -3px;
content: "✓";
color: #000;
}
input[type=button].cancel {
background-color: transparent;
border-color: red;
}
input[type=button].save{
background-color: #111;
float: right;
}
input[type=button].black, input[type=submit].black{
background-color: #000;
border-color: #000;
}
input[type=button].center{
margin-right: auto;
margin-left: auto;
background-color: #000;
border-color: #000;
}
.pointer {
cursor: pointer;
}
.pointer:hover {
color: #00E6FF;
cursor: pointer;
}
.sortThis {
color: #00E6FF;
}
.w40px {
max-width: 40px;
}
.w50px {
max-width: 50px;
}
.w80px {
max-width: 80px;
}
.w150px {
max-width: 150px;
}
.w200px {
max-width: 200px;
min-width: 100px;
width: 200px;
overflow-x: hidden;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.w300px {
max-width: 300px;
}
.w220px {
max-width: 220px;
cursor: alias;
}
.footer {
font-size: 10px;
}
.center {
text-align: center;
}
.screenLogHidden {
transform: translate(0px, -110px);
}
.borderSpace {
margin-bottom: 30px;
}
.block {
}
.none {
display: none;
}
.notVisible {
height: 0px;
display: none;
opacity: 0;
border-bottom: #000 solid 0px;
}
.visible {
opacity: 1;
display: block;
border-bottom: #444 solid 1px;
padding: 10px;
}
.floatRight {
float: right;
}
.floatLeft {
float: left;
}
.menu-active {
background-color: #00E6FF;
}
.menu-notActive {
}
#branch {
display: table;
margin: auto;
color: red;
}
#interaction {
margin-bottom: 100px;
text-align: center;
border-bottom: solid 0px #777;
}
.half {
display: block;
width: 45%;
}
.menu {
border: solid 1px #00E6FF;
}
.infoMsg {
color: #aaa;
}
.errorMsg {
color: red;
}
.warningMsg {
color: yellow;
}
.debugMsg {
color: magenta;
}
.News, .Movie, .Series, .Sports, .Kids {
border-left: solid 2px
}
.News {
border-color: tomato
}
.Movie {
border-color: royalblue;
}
.Series {
border-color: gold;
}
.Sports {
border-color: yellowgreen;
}
.Kids {
border-color: mediumpurple;
}
/* Loading */
#loading {
left: 0px;
top: 0px;
z-index: 10000;
position: absolute;
background-color: rgba(0,0,0, 0.8);
margin: auto;
width: 100%;
height: 100%;
}
.loader {
border: 5px solid transparent;
border-radius: 50%;
border-top: 5px solid #00E6FF;
border-bottom: 5px solid #00E6FF;
width: 50px;
height: 50px;
-webkit-animation: spin 1.2s linear infinite;
animation: spin 1.2s linear infinite;
position: fixed;
margin: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

540
html/css/screen.css Normal file
View File

@@ -0,0 +1,540 @@
nav img {
display: block;
max-height: 20px;
max-width: 20px;
float: left;
}
nav p {
text-align: left;
padding: 0px 30px;
}
#layout {
display: block;
height: 100%;
}
.layout-left {
display: block;
min-width: 150px;
max-width: 20%;
background-color: #111;
height: inherit;
float: left;
}
.layout-right {
display: block;
background-color: #444;
}
#menu-wrapper {
height: 100%;
}
#logo {
display: block;
min-width: 180px;
width: 100%;
height: 100px;
background: url("../img/logo_w_600x200.png");
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
}
#page {
max-width: 950px;
margin: auto;
background-color: #444;
/*
height: -webkit-calc(100% - 130px);
height: -moz-calc(100% - 130px);
height: calc(100% - 130px);
*/
min-height: -webkit-calc(100% - 120px);
min-height: -moz-calc(100% - 120px);
min-height: calc(100% - 120px);
box-shadow: 0px 5px 5px #222;
}
#uiSetting {
float: right;
margin-right: 25px;
}
#box input[type=text], #box input[type=password] {
width: -webkit-calc(100% - 20px);
width: -moz-calc(100% - 20px);
width: calc(100% - 20px);
}
#box input[type=submit]{
margin: 50px auto;
}
#settings {
display: block;
padding: 10px 10px;
}
#settings h5 {
margin: 50px 0px 10px 0px;
}
#content-interaction .search {
width: 200px;
border: 1px solid #000;
padding: 9px;
background-color: #333;
margin: 10px;
float: right;
border-radius: 3px;
}
#myStreams {
position: fixed;
bottom: 0px;
background-color: #111;
width: 100%;
max-width: 950px;
/*
max-height: 100px;
*/
margin-bottom: 0px;
}
#myStreams img {
width: 4%;
padding: 2px 5px;
cursor: pointer;
float: right;
}
#settings-footer {
}
/* Wizard*/
#box {
background-color: #444;
min-height: 400px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#box p{
padding: 10px 0px;
}
#box-footer {
margin-top: auto;
}
#box-footer {
margin: auto;
padding: 10px;
}
#headline {
background-color: #222;
border-bottom: solid 2px #222;
transition: all 0.5s;
padding: 10px 0px;
display: block;
}
#content {
display: block;
overflow: auto;
padding: 10px;
}
/* --- */
#clientInfo, #activeStreams, #inactiveStreams {
font-family: monospace;
display: block;
font-size: 9px;
background-color: #111;
color: #00E6FF;
border-bottom: solid 0px;;
padding: 0px;
letter-spacing: 1px;
overflow-x: hidden;
border-spacing: 4px 4px;
border-bottom: solid 1px #444;
}
#myStreamsBox {
position: relative;
padding: 0px;
/*height: 100px;*/
max-height: 150px;
background-color: #111;
color: white;
display:flex;
justify-content:center;
align-items:center;
}
#openStreams {
width: 20px;
height: 20px;
cursor: pointer;
float: right;
position: absolute;
right: 0px;
bottom: 0px;
background: url("../img/touch.png");
background-color: #111;
background-position: bottom right;
}
#allStreams {
width: 100%;
height: 100%;
padding: 2px;
}
#activeStreams, #inactiveStreams {
overflow-y: scroll;
width: 50%;
max-height: 100px;
float: left;
}
#activeStreams .tdKey, #inactiveStreams .tdKey {
width: 75px;
}
#inactiveStreams .tdKey {
color: red;
}
#clientInfo .tdVal, #logInfo .tdVal, #activeStreams .tdVal, #inactiveStreams .tdVal, #mappingInfo .tdVal{
color: #aaa;
white-space: inherit;
}
#box-wrapper {
display: inline-block;
width: 100%;
overflow-y: scroll;
}
#content_table, #mapping-detail-table, #content_table {
display: table;
border-collapse: collapse;
overflow-y: scroll;
width: 100%;
}
#content_table .content_table_header {
background-color: #333;
height: 50px;
border-bottom: solid 1px #111;
border-left: solid 3px #333;
cursor: auto;
}
tbody {
width: 100%;
}
.tableEllipsis {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#content_table img {
display: block;
max-height: 28px;
margin-left: auto;
margin-right: auto;
max-width: 30px;
}
#content_table tr{
border-left: solid 3px 444;
border-bottom: solid 1px #333;
cursor: pointer;
}
#content_table tr:hover {
background-color: #333;
}
#content_table td {
padding: 0px 2px;
}
#content_table input[type=text]{
width: 80%;
border: 0px;
background-color: #333;
margin-left: 5px;
text-align: left;
}
#content_table input[type=checkbox]{
max-width: 25px;
margin: auto;
}
.showBulk {
display: block;
}
.hideBulk {
display: none;
}
.noBulk {
}
#content_table tr.activeEPG{
border-left: solid 3px lawngreen;
}
#content_table tr.notActiveEPG{
border-left: solid 3px red;
}
#logScreen p{
white-space: pre;
font-size: 10px;
/*
line-height: 1.6em;
font-family: "Arial", sans-serif;
*/
letter-spacing: 1px;
font-family: monospace;
font-size: 12px;
font-style: normal;
font-variant: normal;
line-height: 1.6em;
}
#popup {
background-color: rgba(0, 0, 0, 0.4);
position: fixed;
left: 0px;
width: 100%;
z-index: 100;
height: 100%;
}
#mapping-detail, #user-detail, #file-detail, #popup-custom {
box-shadow: 0px 5px 40px #000;
margin-top: 20px;
margin-left: auto;
margin-right: auto;
max-width: 600px;
background-color: #222;
padding: 10px;
overflow:auto;
}
#popup-custom h3 {
text-align: center;
}
#file-detail input[type=text] {
width: -webkit-calc(100% - 20px);
width: -moz-calc(100% - 20px);
width: calc(100% - 20px);
}
#mapping-detail img {
display: block;
max-height: 30px;
margin-bottom: 20px;
margin-left: auto;
margin-right: auto;
}
#popup-custom input[type=text], #popup-custom input[type=password], #mapping-detail input[type=text], #content_settings input[type=text], #content_settings input[type=password]{
border: solid 1px;
border-color: transparent;
background-color: #333;
text-align: left;
width: -webkit-calc(100% - 20px);
width: -moz-calc(100% - 20px);
width: calc(100% - 20px);
}
#popup-custom input[type=text].notAvailable {
border-color: red;
color: #666;
cursor: not-allowed;
}
#mapping-detail-table, #user-detail-table {
display: inline-table;
width: 100%;
}
#popup-custom table, #content_settings table {
display: inline-table;
table-layout: fixed;
width: 100%;
}
#mapping-detail-table td, #user-detail-table td {
padding: 10px 0px;
}
#mapping-detail-table td.left, #user-detail-table td.left, #popup-custom td.left {
width: 38%;
}
.interaction, #interaction {
margin-top: 20px;
display: inline-flex;
float: right;
}
.interaction input[type=button], .interaction input[type=submit] {
background-color: #000;
min-width: 100px;
margin: 0px 10px;
text-align: center;
}
#notification {
display: block;
position: fixed;
right: 0px;
height: 100%;
width: 250px;
background-color: #222;
box-shadow: 0px 0px 20px #000;
}
#notification h5 {
background-color: #121212;
padding: 5px 10px 5px 10px;
}
#notification pre {
padding: 0px 10px 0px 10px;
}
#notification p {
font-size: 10 px;
margin: 0px;
padding: 0px 10px 5px 10px;
}
#notification .element {
/*padding: 0px 5px;*/
margin: 5px 5px;
border-radius: 5px;
background-color: #181818;
border-left: 10px solid green;
}
@media only screen and (min-width: 620px){
body {
width: 100%;
background-color: #444;
}
h1 {
font-size: 26px;
letter-spacing: 3px;
}
nav p {
display: block;
}
#header_config {
display: block;
height: 100px;
background: url("../img/logo_w_600x200.png");
background-repeat: no-repeat;
background-size: 300px 100px;
}
#screenLog {
margin-left: 300px;
transition: none;
background-color: transparent;
border-bottom: solid 1px transparent;
box-shadow: 0px 0px 0px #222;
}
#settings {
/*
height: -webkit-calc(100% - 100px);
height: -moz-calc(100% - 100px);
height: calc(100% - 100px);
*/
position: relative;
overflow: auto;
}
.screenLogHidden {
transform: translate(0px, 0px);
}
#box {
display: block;
min-height: 500px;
max-width: 500px;
margin: 10px auto;
background-color: #444;
box-shadow: 0px 5px 5px #222;
display: flex;
flex-direction: column;
}
#settings, #settings-footer {
}
}

3
html/css/screen2.css Normal file
View File

@@ -0,0 +1,3 @@
h1 {
color: green;
}

BIN
html/img/BC-QR.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
html/img/filter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
html/img/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
html/img/logo_w_600x200.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
html/img/logout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
html/img/m3u.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
html/img/mapping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
html/img/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
html/img/stream-limit.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
html/img/users.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
html/img/x_ transparent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
html/img/x_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
html/img/x_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
html/img/xmltv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

108
html/index.html Normal file
View File

@@ -0,0 +1,108 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<!---
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-->
<title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css">
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/menu_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/settings_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/logs_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
</head>
<body onload="javascript: PageReady();">
<div id="loading" class="none">
<div class="loader"></div>
</div>
<div id="popup" class="none">
<div id="popup-custom"></div>
</div>
<div id="layout">
<!--
<div id="notification">
<div class="element">
<h5>XEPG</h5>
<pre>11.05.2019 - 20:21</pre>
<hr>
<p>Hallo das ist ein Test. Und noch mehr Text.</p>
</div>
</div>
-->
<div id="menu-wrapper" class="layout-left">
<div id= "branch"></div>
<div id="logo"></div>
<nav id="main-menu"></nav>
</div>
<div class="layout-right">
<table id="clientInfo" class="">
<tr>
<td class="tdKey">xTeVe:</td>
<td id="version" class="tdVal">&nbsp;</td>
<td class="tdKey">OS:</td>
<td id="os" class="tdVal">&nbsp;</td>
<td class="tdKey phone">DVR IP:</td>
<td id="DVR" class="tdVal phone">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">UUID:</td>
<td id="uuid" class="tdVal">&nbsp;</td>
<td class="tdKey">Arch:</td>
<td id="arch" class="tdVal">&nbsp;</td>
<td class="tdKey phone">M3U URL:</td>
<td id="m3u-url" class="tdVal phone">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">Available Streams:</td>
<td id="streams" class="tdVal">&nbsp;</td>
<td class="tdKey">EPG Source:</td>
<td id="epgSource" class="tdVal">&nbsp;</td>
<td class="tdKey phone">XEPG URL:</td>
<td id="xepg-url" class="tdVal phone">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">XEPG Channels:</td>
<td id="xepg" class="tdVal">&nbsp;</td>
<td class="tdKey">Errors:</td>
<td id="errors" class="tdVal">&nbsp;</td>
<td class="tdKey">Warnings:</td>
<td id="warnings" class="tdVal">&nbsp;</td>
</tr>
</table>
<div id="myStreamsBox" class="notVisible">
<div id="allStreams">
<table id="activeStreams"></table>
<table id="inactiveStreams"></table>
</div>
</div>
<div id="content" class=""></div>
</div>
</div>
</body>
</html>

42
html/js/authentication.js Normal file
View File

@@ -0,0 +1,42 @@
function createFirstAccount(elm) {
var err = false;
var div = document.getElementById(elm);
console.log(div);
var form = document.getElementById('authentication');
const username = document.getElementById('username');
const password = document.getElementById('password');
const confirm = document.getElementById('confirm');
var inputs = div.getElementsByTagName('INPUT')
console.log(confirm);
switch(confirm) {
case null: break;
default:
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].value.length == 0) {
inputs[i].style.borderColor = 'red';
err = true
}
}
switch(err) {
case true: return; break;
case false:
if (password.value != confirm.value) {
confirm.style.borderColor = 'red';
return;
}
break;
}
}
form.submit();
return;
}

View File

@@ -0,0 +1,32 @@
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;
}
data[key] = value;
}
if (err == true) {
data = new Object();
return;
}
if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) {
alert("sdafsd");
document.getElementById('password').style.borderColor = "red";
document.getElementById('confirm').style.borderColor = "red";
document.getElementById("err").innerHTML = "{{.account.failed}}";
return;
}
}
console.log(data);
form.submit();
}

331
html/js/base.js Normal file
View File

@@ -0,0 +1,331 @@
var config = new Object();
var menu = new Object();
var subMenu = new Object();
var activeStreams = new Object();
var xEPG = new Object();
var users = new Object();
var log = new Object();
var undo = new Object();
var webSockets = true;
var closeLog, version, activeMenu;
var columnToSort = 0
if (window.WebSocket === undefined) {
alert("Your browser does not support WebSockets");
webSockets = false;
}
function pageReady() {
var data = new Object();
data["cmd"] = "getServerConfig";
xTeVe(data);
//showLoadingScreen(false);
var resizeHandle = document.getElementById("openStreams");
var box = document.getElementById("myStreamsBox");
resizeHandle.addEventListener("mousedown", initialiseResize, false);
function initialiseResize(e) {
window.addEventListener("mousemove", startResizing, false);
window.addEventListener("mouseup", stopResizing, false);
}
function startResizing(e) {
box.style.height = (e.clientY - box.offsetTop) + "px";
var elm = document.getElementById("allStreams");
if (e.clientY > 120) {
elm.className = "visible";
} else {
elm.className = "notVisible";
}
calculateWrapperHeight();
}
function stopResizing(e) {
window.removeEventListener('mousemove', startResizing, false);
window.removeEventListener('mouseup', stopResizing, false);
calculateWrapperHeight();
}
window.addEventListener("resize", function(){
calculateWrapperHeight();
}, true);
}
function getObjKeys(obj) {
var keys = new Array();
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
keys.push(i);
}
}
return keys;
}
function createElement(item) {
//console.log(item);
var element = document.createElement(item["_element"]);
if (item.hasOwnProperty("_text")) {
//element.innerHTML = "<p>" + item["_text"] + "</p>";
element.innerHTML = item["_text"];
}
var keys = getObjKeys(item);
for (var i = 0; i < keys.length; i++) {
if (keys[i].charAt(0) != "_") {
//console.log(keys[i], item[keys[i]]);
element.setAttribute(keys[i], item[keys[i]]);
}
}
//console.log(element);
return element;
}
function modifyOption(id, options, values) {
var select = document.getElementById(id);
select.innerHTML = "";
for (var i = 0; i < options.length; i++) {
var element = document.createElement("OPTION")
element.value = values[i];
element.innerHTML = options[i];
document.getElementById(id).appendChild(element);
}
}
function startWebSocket() {
if (webSockets == false) {
return;
}
//ws.send('{"cmd": "getServerConfig1"}');
}
function checkErr(obj) {
//alert(obj["err"])
//screenLog(obj["err"], "error")
console.log(obj);
var newObj = new Object();
var newErr = new Object();
newErr["key"] = "Error";
newErr["value"] = obj["err"];
newErr["type"] = "error";
newObj[0] = newErr
showLog(newObj);
return
}
function screenLog(msg, msgType, show) {
return
clearTimeout(closeLog)
var div = document.getElementById("screenLog");
var newMsg = new Object();
newMsg["_element"] = "P";
switch(msgType) {
case "error": newMsg["class"] = "errorMsg"; break;
case "warning": newMsg["class"] = "warningMsg"; break;
//default: newMsg["class"] = "infoMsg"
}
newMsg["_text"] = msg;
div.appendChild(createElement(newMsg));
div.scrollTop = div.scrollHeight;
if (show == false) {
return;
}
div.className = ""
closeLog = setTimeout(closeScreenLog, 10000);
}
function closeScreenLog() {
var div = document.getElementById("screenLog");
div.className = "screenLogHidden"
}
function showScreenLog() {
clearTimeout(closeLog)
var div = document.getElementById("screenLog");
var currentClass = div.className;
div.className = "screenLogHidden"
switch(currentClass) {
case "screenLogHidden": div.className = ""; break;
case "": div.className = "screenLogHidden"; break;
}
}
function showLoadingScreen(elm) {
var div = document.getElementById("loading");
switch(elm) {
case true: div.className = "block"; break;
case false: div.className = "none"; break;
/*
case true: div.style.display = "block"; break;
case false: div.style.display = "none"; break;
*/
}
}
function createClintInfo(obj) {
//console.log(obj);
var keys = getObjKeys(obj);
for (var i = 0; i < keys.length; i++) {
if(document.getElementById(keys[i])){
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
}
}
//document.getElementById("clientInfo").className = "visible";
}
function showElement(elmID, type) {
switch(type) {
case true: cssClass = "block"; break;
case false: cssClass = "none"; break;
}
document.getElementById(elmID).className = cssClass;
}
function showPopUpElement(elm) {
var allElements = new Array("deleteUserDetail", "mapping-detail", "user-detail", "file-detail");
for (var i = 0; i < allElements.length; i++) {
showElement(allElements[i], false)
}
showElement(elm, true)
setTimeout(function(){
showElement("popup", true);
}, 10);
}
// body...
function showStreams(force) {
var elmBox = document.getElementById("myStreamsBox");
var elm = document.getElementById("allStreams");
//console.log(elm);
show = elm.className;
switch(force) {
case true: show = "notVisible"; break;
case false: show = "visible"; break;
}
switch(show) {
case "notVisible":
elm.className = "visible";
elmBox.style.height = "100px";
break;
default:
elm.className = "notVisible";
elmBox.style.height = "20px";
break;
}
var show = elm.style.display; {
//console.log(elm.style.display);
}
calculateWrapperHeight();
}
function xteveBackup() {
console.log("xteveBackup");
var data = new Object();
data["cmd"] = "xteveBackup";
xTeVe(data);
}
function xteveRestore(elm) {
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
//console.log(restore.srcElement.files[0]);
var check = confirm("File: " + filename + "\nAll data will be replaced with those from the backup.\nShould 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();
data["cmd"] = "xteveRestore"
data["base64"] = reader.result
xTeVe(data);
return
};
} else {
alert("File could not be loaded")
}
}
};
}
function getBase64(file) {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function() {
console.log(reader.result);
};
reader.onerror = function(error) {
console.log('Error: ', error);
};
}
function logout() {
document.cookie.split(';').forEach(function(c) {
document.cookie = c.trim().split('=')[0] + '=;' + 'expires=Thu, 01 Jan 1970 00:00:00 UTC;';
});
location.reload();
}
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
function setCookie(token) {
//console.log(token);
document.cookie = "Token=" + token
}

473
html/js/base_ts.js Normal file
View File

@@ -0,0 +1,473 @@
var SERVER = new Object();
var BULK_EDIT = false;
var COLUMN_TO_SORT;
var SEARCH_MAPPING = new Object();
var UNDO = new Object();
var SERVER_CONNECTION = false;
var WS_AVAILABLE = false;
// Menü
var menuItems = new Array();
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"));
//menuItems.push(new MainMenuItem("pmsID", "{{.mainMenu.item.pmsID}}", "number.png", "{{.mainMenu.headline.pmsID}}"))
menuItems.push(new MainMenuItem("filter", "{{.mainMenu.item.filter}}", "filter.png", "{{.mainMenu.headline.filter}}"));
menuItems.push(new MainMenuItem("xmltv", "{{.mainMenu.item.xmltv}}", "xmltv.png", "{{.mainMenu.headline.xmltv}}"));
menuItems.push(new MainMenuItem("mapping", "{{.mainMenu.item.mapping}}", "mapping.png", "{{.mainMenu.headline.mapping}}"));
menuItems.push(new MainMenuItem("users", "{{.mainMenu.item.users}}", "users.png", "{{.mainMenu.headline.users}}"));
menuItems.push(new MainMenuItem("settings", "{{.mainMenu.item.settings}}", "settings.png", "{{.mainMenu.headline.settings}}"));
menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.mainMenu.headline.log}}"));
menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"));
// Kategorien für die Einstellungen
var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.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;
}
document.getElementById(elmID).className = cssClass;
}
function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value;
document.getElementById(buttonID).setAttribute(attribute, value);
}
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 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;
}
}
}
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;
}
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 == "" || xValue == NaN) {
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"];
channels.forEach(function (id) {
channelKeys.forEach(function (key) {
if (key == "x-active") {
switch (data[id][key]) {
case true:
SEARCH_MAPPING[id] = "online ";
break;
case false:
SEARCH_MAPPING[id] = "offline ";
break;
}
}
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() {
if (document.getElementById("box-wrapper")) {
var elm = document.getElementById("box-wrapper");
var divs = new Array("myStreamsBox", "clientInfo", "content");
var elementsHeight = 0 - elm.offsetHeight;
for (var i = 0; i < divs.length; i++) {
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
}
elm.style.height = window.innerHeight - elementsHeight + "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)) {
alert("{{.alert.invalidChannelNumber}}");
return;
}
channels.forEach(function (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(function (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) {
alert(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 + "\n{{.confirm.restore}}");
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 {
alert("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 {
alert("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() {
console.log("TOKEN");
var server = new Server("updateLog");
server.request(new Object());
}

40
html/js/classes_ts.js Normal file
View File

@@ -0,0 +1,40 @@
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var MainMenu = /** @class */ (function () {
function MainMenu() {
this.DocumentID = "main-menu";
this.HTMLTag = "LI";
}
MainMenu.prototype.create = function () {
console.log(this.DocumentID);
};
return MainMenu;
}());
var MainMenuItem = /** @class */ (function (_super) {
__extends(MainMenuItem, _super);
function MainMenuItem() {
return _super !== null && _super.apply(this, arguments) || this;
}
MainMenuItem.prototype.create2 = function () {
var element = document.createElement(this.HTMLTag);
element.innerText = this.Value;
console.log(element);
};
return MainMenuItem;
}(MainMenu));
function pageReady() {
var item = new MainMenuItem();
item.Value = "Test";
item.create2();
}

294
html/js/configuaration.js Normal file
View File

@@ -0,0 +1,294 @@
var configMenu = new Object();
var wizard = new Array("key", "tuner", "epgSource", "m3u", "complete");
var activeWizard;
var dvrIP
var configMenu_tuner = new Object();
configMenu_tuner["_element"] = "SELECT";
configMenu_tuner["_menuType"] = "singleInput";
configMenu_tuner["_configKey"] = "tuner";
configMenu_tuner["_label"] = "Available tuners";
configMenu_tuner["name"] = "tuner";
configMenu_tuner["id"] = "Tuner";
configMenu_tuner["placeholder"] = "Tuner";
configMenu_tuner["_usage"] = "This setting is only used by Plex and Emby.<br>The number of concurrent streams allowed by the IPTV provider."
var optionValues = new Array();
for (var i = 1; i <= 100; i++) {
optionValues.push(i)
}
configMenu_tuner["_optionValues"] = optionValues;
var configMenu_epg = new Object();
configMenu_epg["_element"] = "SELECT";
configMenu_epg["_menuType"] = "singleInput";
configMenu_epg["_configKey"] = "epgSource";
configMenu_epg["_label"] = "Selection of the EPG source";
configMenu_epg["name"] = "epgSource";
configMenu_epg["id"] = "EPG source";
configMenu_epg["placeholder"] = "EPG source";
configMenu_epg["_optionValues"] = new Array("PMS", "XEPG");
configMenu_epg["_usage"] = "PMS: Use EPG data from Plex or Emby<br>XEPG: Use of external EPG data (XMLTV)<br> Several XMLTV sources possible<br> Allows editing and order channels<br> M3U / XMLTV export (HTTP link for IPTV apps)"
var configMenu_m3u = new Object();
configMenu_m3u["_element"] = "INPUT";
configMenu_m3u["_menuType"] = "inputArray";
configMenu_m3u["_configKey"] = "file";
configMenu_m3u["_label"] = "M3U File: local or remote";
configMenu_m3u["name"] = "file";
configMenu_m3u["id"] = "m3u";
configMenu_m3u["type"] = "text";
configMenu_m3u["placeholder"] = "M3U File";
configMenu_m3u["_usage"] = "Remote playlist: http://your.provider.com/file.m3u<br>Local playlist: /path/to/file.m3u"
configMenu_m3u["value"] = "http://websrv.local:8080/kabel.m3u";
var configMenu_complete = new Object();
configMenu_complete["_element"] = "H2";
configMenu_complete["_menuType"] = "inputArray";
configMenu_complete["_configKey"] = "file";
configMenu_complete["_text"] = "xTeVe was successfully set up";
configMenu_complete["name"] = "complete";
configMenu_complete["id"] = "complete";
configMenu_complete["type"] = "text";
configMenu_complete["class"] = "center";
configMenu["tuner"] = configMenu_tuner;
configMenu["epgSource"] = configMenu_epg;
configMenu["m3u"] = configMenu_m3u;
configMenu["complete"] = configMenu_complete;
function readyForConfiguration() {
var data = new Object();
data["cmd"] = "getServerConfig";
xTeVe(data);
showLoadingScreen(false);
}
function createConfiguration(elm) {
activeWizard = elm;
var item = configMenu[elm];
var div = document.getElementById("content");
div.innerHTML = "";
div.setAttribute("data-configKey", item["_configKey"]);
div.setAttribute("data-menuType", item["_menuType"]);
switch(item.hasOwnProperty("_label")) {
case true:
var newItem = new Object();
newItem["_element"] = "LABEL";
newItem["_text"] = item["_label"];
newItem["for"] = item["id"];
div.appendChild(createElement(newItem));
break
}
switch(item["_element"]) {
case "SELECT":
div.appendChild(createElement(item));
var selectElement = div.getElementsByTagName("SELECT")[0];
var values = item["_optionValues"];
for (var i = 0; i < values.length; i++) {
var newEntry = new Object;
newEntry["_element"] = "OPTION";
newEntry["_text"] = item["id"] + ": " + values[i];
newEntry["value"] = values[i];
selectElement.appendChild(createElement(newEntry));
}
//return
break;
default:
div.appendChild(createElement(item));
break;
}
//alert()
switch(item.hasOwnProperty("_usage")) {
case true:
var usageItem = new Object();
usageItem["_element"] = "PRE"
usageItem["_text"] = item["_usage"];
div.appendChild(createElement(usageItem));
}
if (activeWizard == "complete") {
document.getElementById("next").value = "Finished"
//document.getElementById("next").setAttribute("onclick", "javascript: location.reload();")
}
//div.appendChild(createElement(item));
}
function saveData() {
var div = document.getElementById("content");
var inputs = div.getElementsByTagName("INPUT");
var selects = div.getElementsByTagName("SELECT");
var value;
var data = new Object();
var valueArr = new Array();
var newData = false;
if (activeWizard == "complete") {
data["cmd"] = "wizardCompleted";
showLoadingScreen(true)
xTeVe(data);
return
}
for (var i = 0; i < inputs.length; i++) {
var menuType = inputs[i].parentElement.getAttribute("data-menutype");
if (inputs[i].value != undefined && inputs[i].value != "" ) {
newData = true;
console.log(inputs[i].id)
switch(inputs[i].id) {
case "m3u":
var newPlaylist = new Object();
newPlaylist["file.source"] = inputs[i].value;
//newPlaylist["name"] = inputs[i].value;
newPlaylist["type"] = "m3u";
newPlaylist["new"] = true;
data["files"] = new Object();
data["files"]["m3u"] = new Object();
data["files"]["m3u"]["-"] = newPlaylist;
data["cmd"] = "saveFilesM3U";
xTeVe(data)
return
}
/*
switch(menuType) {
case "singleInput":
data[inputs[i].name] = inputs[i].value; break;
case "inputArray":
valueArr.push(inputs[i].value);
data[inputs[i].name] = valueArr; break
}
*/
} else {
inputs[i].style.borderBottomColor = "red";
return;
}
}
for (var i = 0; i < selects.length; i++) {
var value = selects[i].options[selects[i].selectedIndex].value;
if (isNaN(value) == false) {
value = parseInt(value);
data[selects[i].name] = value;
newData = true;
break;
}
data[selects[i].name] = value;
newData = true;
}
//console.log(data, newData);
if (newData == true) {
config = data
data["cmd"] = "saveConfig";
xTeVe(data);
}
}
function xTeVe(data) {
if (webSockets == false) {
alert("Your browser does not support WebSockets");
return;
}
if (activeWizard == "m3u" || activeWizard == "epgSource") {
showLoadingScreen(true);
}
var protocolWS
switch(window.location.protocol) {
case "http:": protocolWS = "ws://"; break;
case "https:": protocolWS = "wss://"; break;
}
var ws = new WebSocket(protocolWS + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token"));
ws.onopen = function() {
ws.send(JSON.stringify(data));
}
ws.onmessage = function (e) {
var response = JSON.parse(e.data);
if (response.hasOwnProperty("clientInfo")) {
createClintInfo(response["clientInfo"]);
}
if (response.hasOwnProperty("status")) {
if (response["status"] == false) {
document.getElementById("headline").style.borderColor = "red";
showErr(response["err"]);
showLoadingScreen(false)
return
} else {
document.getElementById("err").innerHTML = "";
document.getElementById("headline").style.borderColor = "lawngreen";
}
dvrIP = response["DVR"]
switch(response["configurationWizard"]) {
case true:
if (activeWizard == undefined) {
activeWizard = wizard[0]
}
var n = wizard.indexOf(activeWizard);
n++;
activeWizard = wizard[n]
if (activeWizard == undefined) {
data["cmd"] = "wizardCompleted";
xTeVe(data)
} else {
//console.log(activeWizard);
createConfiguration(activeWizard);
}
break;
}
switch(response["reload"]) {
case true:
setTimeout(function(){
location.reload();
}, 100);
//location.reload();
break;
}
}
setTimeout(function(){ showLoadingScreen(false); }, 300);
}
}
function showErr(elm) {
document.getElementById("err").innerHTML = elm;
}

140
html/js/configuration_ts.js Normal file
View File

@@ -0,0 +1,140 @@
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var WizardCategory = /** @class */ (function () {
function WizardCategory() {
this.DocumentID = "content";
}
WizardCategory.prototype.createCategoryHeadline = function (value) {
var element = document.createElement("H4");
element.innerHTML = value;
return element;
};
return WizardCategory;
}());
var WizardItem = /** @class */ (function (_super) {
__extends(WizardItem, _super);
function WizardItem(key, headline) {
var _this = _super.call(this) || this;
_this.headline = headline;
_this.key = key;
return _this;
}
WizardItem.prototype.createWizard = function () {
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 = "{{.wizard.tuner.description}}";
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 = "{{.wizard.epgSource.description}}";
break;
case "m3u":
var input = content.createInput("text", key, "");
input.setAttribute("class", "wizard");
input.id = key;
doc.appendChild(input);
description = "{{.wizard.m3u.description}}";
break;
case "xmltv":
var input = content.createInput("text", key, "");
input.setAttribute("class", "wizard");
input.id = key;
doc.appendChild(input);
description = "{{.wizard.xmltv.description}}";
break;
default:
console.log(key);
break;
}
var pre = document.createElement("PRE");
pre.innerHTML = description;
doc.appendChild(pre);
console.log(headline, key);
};
return WizardItem;
}(WizardCategory));
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;
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", "{{.wizard.tuner.title}}"));
configurationWizard.push(new WizardItem("epgSource", "{{.wizard.epgSource.title}}"));
configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}"));
configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.title}}"));

329
html/js/data.js Normal file
View File

@@ -0,0 +1,329 @@
function showConfig(obj) {
config = obj;
//setMenuItem();
createMenu();
//document.getElementById("page").className = "";
}
showMyStreams
function showMyStreams(allStreamsObj) {
var streamTypeKeys = getObjKeys(allStreamsObj)
for (var s = 0; s < streamTypeKeys.length; s++) {
var streamType = streamTypeKeys[s];
var obj = new Object();
obj = allStreamsObj[streamType];
switch(streamType) {
case "activeStreams": activeStreams = obj; break;
}
document.getElementById(streamType).innerHTML = "";
var streamsObj = new Object();
var streamsNames = new Array();
var keys = getObjKeys(obj)
// Create Object (streamsObj) for the streams and sort by name (streamsNames)
for (var i = 0; i < keys.length; i++) {
var name = obj[keys[i]]["name"];
var tmp = new Object();
var streamKey = getObjKeys(obj[keys[i]]);
for (var j = 0; j < streamKey.length; j++) {
tmp[streamKey[j]] = obj[keys[i]][streamKey[j]];
}
streamsObj[name] = tmp;
streamsNames.push(name)
}
streamsNames.sort();
// Create Table for activeStreams
var table = document.getElementById(streamType);
for (var i = 0; i < streamsNames.length; i++) {
var newEntry = new Object();
newEntry["_element"] = "TR";
table.appendChild(createElement(newEntry));
var line = table.lastChild;
var tmp = streamsObj[streamsNames[i]]
var keys = getObjKeys(tmp)
var newKey = new Object()
newKey["_element"] = "TD";
//newKey["_text"] = streamsNames[i];
switch(streamType) {
case "activeStreams": newKey["_text"] = "Channel (+):"; break;
case "inactiveStreams": newKey["_text"] = "Channel (-):"; break;
}
newKey["class"] = "tdKey";
console.log();
var newVal = new Object()
newVal["_element"] = "TD";
newVal["_text"] = streamsNames[i];
newVal["class"] = "tdVal";
//newVal["_text"] = value;
line.appendChild(createElement(newKey));
line.appendChild(createElement(newVal));
}
}
return
}
function showActiveStreams(obj) {
document.getElementById("activeStreams").innerHTML = "";
activeStreams = obj;
var streamsObj = new Object();
var streamsNames = new Array();
var keys = getObjKeys(obj)
// Create Object (streamsObj) for the streams and sort by name (streamsNames)
for (var i = 0; i < keys.length; i++) {
var name = obj[keys[i]]["name"];
var tmp = new Object();
var streamKey = getObjKeys(obj[keys[i]]);
for (var j = 0; j < streamKey.length; j++) {
tmp[streamKey[j]] = obj[keys[i]][streamKey[j]];
}
streamsObj[name] = tmp;
streamsNames.push(name)
}
streamsNames.sort();
// Create Table for activeStreams
var table = document.getElementById("activeStreams");
for (var i = 0; i < streamsNames.length; i++) {
var newEntry = new Object();
newEntry["_element"] = "TR";
table.appendChild(createElement(newEntry));
var line = table.lastChild;
var tmp = streamsObj[streamsNames[i]]
var keys = getObjKeys(tmp)
var newKey = new Object()
newKey["_element"] = "TD";
//newKey["_text"] = streamsNames[i];
newKey["_text"] = "Channel:";
newKey["class"] = "tdKey";
console.log();
var newVal = new Object()
newVal["_element"] = "TD";
newVal["_text"] = streamsNames[i];
newVal["class"] = "tdVal";
//newVal["_text"] = value;
line.appendChild(createElement(newKey));
line.appendChild(createElement(newVal));
}
}
function parseLogs(obj) {
log = obj
var keys = getObjKeys(obj)
var msgType;
for (var i = 0; i < keys.length; i++) {
switch(keys[i]) {
case "warnings": msgType = "warningMsg"; break;
case "errors": msgType = "errorMsg"; break;
}
switch(obj[keys[i]]) {
case 0: msgType = "tdVal"; break;
default: break;
}
if(document.getElementById(keys[i])){
document.getElementById(keys[i]).className = msgType;
}
}
return
}
function cancelData(element) {
createMenu();
}
function saveData(element) {
var data = new Object();
var div = element.parentNode.parentNode;
var inputs = div.getElementsByTagName("INPUT");
var configKey = div.getAttribute("data-configkey");
var menuType = div.getAttribute("data-menutype");
var value;
var valueArr = new Array();
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].type == "text" && inputs[i].value != undefined && inputs[i].value != "" ) {
console.log(inputs[i].value, menuType)
switch(menuType) {
case "inputArray": valueArr.push(inputs[i].value); break;
case "singleInput": value = inputs[i].value; break;
}
}
}
switch(menuType) {
case "inputArray": data[configKey] = valueArr; break;
case "singleInput":
if (isNaN(value) == false) {
value = parseInt(value);
data[configKey] = value;
break;
}
if (value == undefined) {
data["delete"] = configKey;
} else {
data[configKey] = value
}
break;
}
data["cmd"] = "saveConfig";
console.log(data);
xTeVe(data)
}
function xTeVe(data) {
if (webSockets == false) {
alert("Your browser does not support WebSockets");
return;
} else {
if (data["cmd"] != "getLog") {
showLoadingScreen(true)
}
}
delete undo["epgMapping"];
var protocolWS
switch(window.location.protocol) {
case "http:": protocolWS = "ws://"; break;
case "https:": protocolWS = "wss://"; break;
}
var ws = new WebSocket(protocolWS + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token"));
ws.onopen = function() {
console.log(data)
ws.send(JSON.stringify(data));
}
ws.onmessage = function (e) {
var response = JSON.parse(e.data);
console.log(response);
if (response.hasOwnProperty("clientInfo")) {
createClintInfo(response["clientInfo"]);
}
if (response.hasOwnProperty("log")) {
createClintInfo(response["log"]);
}
if (response.hasOwnProperty("status")) {
if (response["status"] == false) {
alert(response["err"])
if(response.hasOwnProperty("reload")) {
location.reload();
}
//checkErr(response)
console.log(response);
updateXteveStatus(response);
setTimeout(function(){ showLoadingScreen(false); }, 300);
return
}
updateXteveStatus(response)
//console.log(data["cmd"]);
switch(data["cmd"]) {
case "saveUserData": createMenu(); break;
case "saveNewUser": createMenu(); break;
case "saveFilesXMLTV": //createMenu(); break;
case "saveFilesM3U": //createMenu(); return; break;
case "saveConfig":
data = new Object();
data["cmd"] = "checkToken";
xTeVe(data);
break;
case "emptyLog": writeLogInDiv(); break;
case "getLog": return; break;
}
}
if (config["files"] == undefined || config["files"].length == 0) {
createMenu();
document.getElementById(10).click()
}
setTimeout(function(){ showLoadingScreen(false); }, 0);
}
}
function updateXteveStatus(response) {
var keys = getObjKeys(response);
//console.log(keys);
for (var i = 0; i < keys.length; i++) {
switch(keys[i]) {
case "alert": alert(response[keys[i]]); break;
case "config": showConfig(response[keys[i]]); break;
case "log": parseLogs(response[keys[i]]); break;
case "myStreams": showMyStreams(response[keys[i]]); break;
case "xEPG": xEPG = response[keys[i]]; break;
case "users": users = response[keys[i]]; break;
case "token": document.cookie = "Token=" + response[keys[i]]; break;
case "reload": location.reload(); break;
case "openLink": window.location = response["openLink"]; break;
//case "version": version = response[keys[i]]; break;
}
}
}
function getValueFromProviderFile(xXmltvFile, fileType, key) {
var fileID = xXmltvFile.substring(0, xXmltvFile.lastIndexOf('.'))
if (config["files"][fileType].hasOwnProperty(fileID) == true) {
var data = config["files"][fileType][fileID];
return data[key]
}
}

379
html/js/files.js Normal file
View File

@@ -0,0 +1,379 @@
function openFiles(elm, fileType) {
//document.getElementById("settings").innerHTML = "Test";
columnToSort = 0;
var newDiv = document.getElementById("settings");
var newEntry = new Object();
newEntry["_element"] = "HR";
newDiv.appendChild(createElement(newEntry));
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
newEntry["class"] = "button";
newEntry["value"] = "New";
newEntry["onclick"] = 'fileDetail("-", "' + fileType + '")';
newDiv.appendChild(createElement(newEntry));
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
newEntry["class"] = "button";
newEntry["value"] = "Update";
newEntry["onclick"] = "fileDetail(0)";
//newDiv.appendChild(createElement(newEntry));
var div = document.getElementById("settings");
// Build table
var newTable = new Object();
newTable["_element"] = "TABLE";
newTable["id"] = "id_mapping";
newTable["class"] = "table-mapping";
div.appendChild(createElement(newTable));
setTimeout(function(){
createFilesTable(fileType);
}, 10);
}
function createFilesTable(fileType) {
var table = document.getElementById("id_mapping");
var availableFileTypes = new Array();
table.innerHTML = "";
var newTR = new Object();
newTR["_element"] = "TR";
newTR["class"] = "table-mapping-header";
table.appendChild(createElement(newTR));
var tr = table.lastChild;
switch(fileType) {
case "xmltv":
availableFileTypes = new Array("xmltv");
var trHeadlines = new Array("Guide", "Last Update", "Availability %", "Channels", "Programs")
var compatibilityKeys = new Array("xmltv.channels", "xmltv.programs")
break;
case "m3u":
availableFileTypes = new Array("m3u", "hdhr");
var trHeadlines = new Array("Playlist", "Last Update", "Availability %", "Type", "Streams", "group-title %", "tvg-id %", "Unique ID %");
var compatibilityKeys = new Array("streams", "group.title", "tvg.id", "stream.id");
break;
}
for (var i = 0; i < trHeadlines.length; i++) {
var newTD = new Object();
newTD["_element"] = "TD";
newTD["_text"] = trHeadlines[i];
tr.appendChild(createElement(newTD));
}
for (var i = 0; i < availableFileTypes.length; i++) {
var fileType = availableFileTypes[i]
var data = config["files"][fileType];
var allFiles = getObjKeys(data)
for (var f = 0; f < allFiles.length; f++) {
var elm = data[allFiles[f]];
var table = document.getElementById("id_mapping");
var fileID = elm["id.provider"];
var name = elm["name"];
var lastUpdate = elm["last.update"];
var availability = elm["provider.availability"];
var type = elm["type"].toUpperCase();
var compatibility = elm["compatibility"];
// Create TR
var newTR = new Object();
newTR["_element"] = "TR";
newTR["class"] = "";
newTR["id"] = fileID;
newTR["onclick"] = 'javascript: fileDetail("' + fileID + '","' + fileType + '");';
table.appendChild(createElement(newTR));
var tr = table.lastChild;
// Create file name TD
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = name;
createNewTD(newTD, tr);
// Create last update TD
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = lastUpdate;
createNewTD(newTD, tr);
// Create availability TD
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = availability;
createNewTD(newTD, tr);
if (fileType == "m3u" || fileType == "hdhr") {
// Create Type TD
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = type;
createNewTD(newTD, tr);
}
// Create all compatibility TDs
for (var j = 0; j < compatibilityKeys.length; j++) {
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = compatibility[compatibilityKeys[j]];
createNewTD(newTD, tr);
}
}
}
sortTable(0)
// usage Info
var div = document.getElementById("settings");
switch(menu[activeMenu.id].hasOwnProperty("_usage")) {
case true:
var usageItem = new Object();
usageItem["_element"] = "PRE"
usageItem["_text"] = menu[activeMenu.id]["_usage"];
var newHR = new Object();
newHR["_element"] = "HR"
div.appendChild(createElement(newHR));
div.appendChild(createElement(usageItem));
break;
}
calculateWrapperHeight();
return;
}
function fileDetail(fileID, fileType) {
optionsText = new Array("M3U", "HDHomeRun - [Experimental]")
optionsValue = new Array("m3u", "hdhr")
switch (fileType) {
case "m3u":
document.getElementById("name").setAttribute("placeholder", "Playlist name");
document.getElementById("description").setAttribute("placeholder", "Description of this playlist");
document.getElementById("file-detail-headline").innerHTML = "M3U Playlist";
document.getElementById("file-path").innerHTML = "M3U File:";
document.getElementById("file.source").setAttribute("placeholder", "Local or remote");
break;
case "hdhr":
document.getElementById("name").setAttribute("placeholder", "HDHomeRun name");
document.getElementById("description").setAttribute("placeholder", "Description of this HDHomeRun tuner");
document.getElementById("file-detail-headline").innerHTML = "HDHomeRun";
document.getElementById("file-path").innerHTML = "HDHomeRun IP:";
document.getElementById("file.source").setAttribute("placeholder", "IP address and port of the tuner (192.168.1.10:5004)");
break;
case "xmltv":
document.getElementById("name").setAttribute("placeholder", "XMLTV name");
document.getElementById("description").setAttribute("placeholder", "Description of this XMLTV file");
document.getElementById("file-detail-headline").innerHTML = "XMLTV File";
document.getElementById("file-path").innerHTML = "XMLTV File:";
document.getElementById("file.source").setAttribute("placeholder", "Local or remote");
optionsText = new Array("XMLTV")
optionsValue = new Array("xmltv")
break;
}
modifyOption("type", optionsText, optionsValue)
showPopUpElement('file-detail');
document.getElementById("saveFileDetail").setAttribute("onclick", 'javascript: saveFileDetail("' + fileID + '","' + fileType + '", false)');
document.getElementById("updateFileDetail").setAttribute("onclick", 'javascript: updateFile("' + fileID + '","' + fileType + '", false)');
document.getElementById("deleteFileDetail").setAttribute("onclick", 'javascript: saveFileDetail("' + fileID + '","' + fileType + '", true)');
var data = new Object();
switch(fileID) {
case "-": // New file
data["name"] = "";
data["description"] = "";
data["file.source"] = "";
data["type"] = fileType;
document.getElementById("deleteFileDetail").className = "delete";
document.getElementById("type").setAttribute("onchange", "changeFileType(this);")
document.getElementById("type").setAttribute("data-id", fileID)
showElement("deleteFileDetail", false);
showElement("updateFileDetail", false);
if (fileType == "xmltv") {
showElement("type", false);
showElement("file-type", false);
} else {
showElement("type", true);
showElement("file-type", true);
}
break;
default:
data = config["files"][fileType][fileID];
document.getElementById("deleteFileDetail").className = "delete";
showElement("updateFileDetail", true);
showElement("type", false);
showElement("file-type", false);
break;
}
var keys = getObjKeys(data);
for (var i = 0; i < keys.length; i++) {
if(document.getElementById(keys[i])){
document.getElementById(keys[i]).value = data[keys[i]];
}
}
}
function changeFileType(elm) {
var fileID = elm.getAttribute("data-id");
var fileType = elm.options[elm.selectedIndex].value;
fileDetail(fileID, fileType)
}
function saveFileDetail(fileID, fileType, deleteFile) {
if (fileID == undefined) {
alert("ID is missing!!!");
return
}
var inputs = document.getElementById("file-detail").getElementsByTagName("INPUT");
var selects = document.getElementById("file-detail").getElementsByTagName("SELECT");
var newFileData = new Object();
var data = new Object();
for (var i = 0; i < inputs.length; i++) {
switch(inputs[i].type) {
case "text": newFileData[inputs[i].name] = inputs[i].value; break;
}
}
for (var i = 0; i < selects.length; i++) {
newFileData[selects[i].id] = selects[i].options[selects[i].selectedIndex].value;
}
if (deleteFile == true) {
switch(fileType) {
case "m3u": var alertText = "Delete this playlist?"; break;
case "hdhr": var alertText = "Delete this HDHomeRun tuner?"; break;
case "xmltv": var alertText = "Delete this XMLTV file?"; break;
}
if (confirm(alertText)) {
newFileData["delete"] = true
data = buildFilesObj(fileType, fileID, newFileData);
console.log(data);
} else {
showElement("popup", false);
return
}
} else {
switch(config["files"][fileType].hasOwnProperty(fileID)) {
case true:
data = config["files"][fileType][fileID];
if (data["file.source"] != newFileData["file.source"]) {
data["update"] = true
} else {
data["updatePlaylistName"] = true;
}
break;
case false:
newFileData["new"] = true;
data = buildFilesObj(fileType, fileID, newFileData);
break
}
}
switch(fileType) {
case "m3u": data["cmd"] = "saveFilesM3U"; break;
case "hdhr": data["cmd"] = "saveFilesHDHR"; break;
case "xmltv": data["cmd"] = "saveFilesXMLTV"; break;
}
//console.log(data);
xTeVe(data);
return
}
function updateFile(fileID, fileType, allFiles) {
switch(config["files"][fileType].hasOwnProperty(fileID)) {
case true:
var data = new Object();
var data = buildFilesObj(fileType, fileID, config["files"][fileType][fileID])
data["new"] = true
switch(fileType) {
case "m3u": data["cmd"] = "updateFileM3U"; break;
case "hdhr": data["cmd"] = "updateFileHDHR"; break;
case "xmltv": data["cmd"] = "updateFileXMLTV"; break;
}
xTeVe(data);
break;
}
}
function buildFilesObj(fileType, fileID, obj) {
var data = new Object();
data["files"] = new Object();
data["files"][fileType] = new Object();
data["files"][fileType][fileID] = obj
return data
}

114
html/js/log.js Normal file
View File

@@ -0,0 +1,114 @@
var logInterval
function updateLog() {
var data = new Object();
data["cmd"] = "getLog";
xTeVe(data);
writeLogInDiv();
return
}
function writeLogInDiv() {
var logs = log["log"];
var div = document.getElementById("settings").lastChild.lastChild;
div.innerHTML = "";
var max = 50;
for (var i = 0; i < logs.length; i++) {
var newEntry = new Object();
newEntry["_element"] = "P";
if (logs[i].includes("ERROR")) {
// case "warnings": msgType = "warningMsg"; break;
newEntry["class"] = "errorMsg";
}
if (logs[i].includes("WARNING")) {
// case "warnings": msgType = "warningMsg"; break;
newEntry["class"] = "warningMsg";
}
newEntry["_text"] = logs[i];
div.appendChild(createElement(newEntry));
}
calculateWrapperHeight();
var scrollDiv = document.getElementById("box-wrapper");
scrollDiv.scrollTop = scrollDiv.scrollHeight;
}
function showLog(obj) {
//logInterval = setInterval(updateLog, 5000);
var logs = log["log"];
var div = document.getElementById("settings");
var newEntry = new Object();
newEntry["_element"] = "HR";
div.appendChild(createElement(newEntry));
//div = div.lastChild;
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
newEntry["class"] = "button";
newEntry["value"] = "Empty Log";
newEntry["onclick"] = "emptyLog()";
div.appendChild(createElement(newEntry));
var newEntry = new Object();
newEntry["_element"] = "code";
newEntry["_text"] = "Update Log: ";
div.appendChild(createElement(newEntry));
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "checkbox";
//newEntry["checked"] = "checkbox";
newEntry["onclick"] = "logUpdates(this)";
div.appendChild(createElement(newEntry));
var newEntry = new Object();
newEntry["_element"] = "HR";
div.appendChild(createElement(newEntry));
var newWrapper = new Object();
newWrapper["_element"] = "DIV";
newWrapper["id"] = "box-wrapper";
div.appendChild(createElement(newWrapper));
div = div.lastChild;
var newPre = new Object();
newPre["_element"] = "PRE";
newPre["id"] = "logScreen";
div.appendChild(createElement(newPre));
div = div.lastChild;
writeLogInDiv()
return
}
function emptyLog() {
var data = new Object();
data["cmd"] = "emptyLog";
xTeVe(data);
return
}
function logUpdates(elm) {
switch(elm.checked) {
case false: clearInterval(logInterval); break;
case true: logInterval = setInterval(updateLog, 5000); break;
}
}

42
html/js/logs_ts.js Normal file
View File

@@ -0,0 +1,42 @@
var Log = /** @class */ (function () {
function Log() {
}
Log.prototype.createLog = function (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.innerHTML = entry;
return element;
};
return Log;
}());
function showLogs(bottom) {
var log = new Log();
var logs = SERVER["log"]["log"];
var div = document.getElementById("content_log");
div.innerHTML = "";
var keys = getObjKeys(logs);
keys.forEach(function (logID) {
var entry = log.createLog(logs[logID]);
div.append(entry);
});
setTimeout(function () {
if (bottom == true) {
var wrapper = document.getElementById("box-wrapper");
wrapper.scrollTop = wrapper.scrollHeight;
}
}, 10);
}
function resetLogs() {
var cmd = "resetLogs";
var data = new Object();
var server = new Server(cmd);
server.request(data);
}

1466
html/js/mapping-editor.js Normal file

File diff suppressed because it is too large Load Diff

754
html/js/menu.js Normal file
View File

@@ -0,0 +1,754 @@
function setMenuItem() {
menu = new Object();
subMenu = new Object();
var menu_m3u = new Object();
menu_m3u["_menuType"] = "inputArray";
menu_m3u["_element"] = "LI";
menu_m3u["_configKey"] = "files.m3u";
menu_m3u["_text"] = "Playlist";
menu_m3u["_icon"] = "img/m3u.png";
menu_m3u["_headline"] = "Playlists: Local or remote";
menu_m3u["_usage"] = "<b>Info</b><br>Availability: File availability in percent<br>Streams: Number of streams in the file.<br>group-title: Streams that are assigned to a group. Simplifies filtering streams<br>tvg-id: This ID is used for automatic mapping, must match with the channel ID in the XMLTV file.<br>Unique ID: Streams with a unique ID to identify them. Allows channel name changes in the M3U without losing the XMLTV mapping (PPV / live events).<br><br><b>Usage M3U:</b><br>Remote playlist: http://your.iptv.provider.com/file.m3u<br>Local playlist: /path/to/file.m3u<br><br><b>Usage HDHomeRun:</b><br>IP: 192.168.1.10:5004<br>"
menu_m3u["name"] = "file";
menu_m3u["id"] = "file";
menu_m3u["value"] = menu_m3u["name"];
menu_m3u["placeholder"] = "Playlist: local or remote";
menu_m3u["onclick"] = "javascript: toggleMenu(this);";
menu_m3u["class"] = "menu-notActive";
var menu_filter = new Object();
menu_filter["_menuType"] = "inputArray";
menu_filter["_element"] = "LI";
menu_filter["_configKey"] = "filter";
menu_filter["_text"] = "Filter";
menu_filter["_icon"] = "img/filter.png";
menu_filter["_headline"] = "Filter by M3U parameters, e.g. group-title";
menu_filter["_usage"] = "<b>Usage:</b><br>Sport - All sports channels<br>Sport {HD} - All HD sports channels<br>Sport {HD} !{ES,DE} - All HD sports channels, but no Spanish and German<br><br>To filter the streams of a HDHomeRun, the playlist name can be entered:<br>My tuner {HD}"
//menu_filter["_usage"] = "<b>Usage:</b><br>All sports channels: Sport<br>All HD sports channels: Sport {HD}<br>All HD sports channels, but no Spanish and German: Sport {HD} !{ES,DE}"
menu_filter["name"] = "filter";
menu_filter["id"] = "M3U";
menu_filter["value"] = menu_filter["name"];
menu_filter["placeholder"] = "Filter streams: Sport";
menu_filter["onclick"] = "javascript: toggleMenu(this);";
menu_filter["class"] = "menu-notActive";
var menu_id = new Object();
menu_id["_menuType"] = "inputArray";
menu_id["_element"] = "LI";
menu_id["_configKey"] = "id";
menu_id["_text"] = "PMS ID";
menu_id["_icon"] = "img/number.png";
menu_id["_headline"] = "Setup PMS guide number";
menu_id["_usage"] = 'Some playlists have unique channel IDs.<br>Enter the keyword of the ID. The channel assignment in PMS will change as a result.<br><br>e.g. channelID<br>#EXTINF:0 type="stream" <b>channelId</b>="81", My Streaming Channel HD<br><br>Only enter here if you know what you are doing!'
menu_id["name"] = "id";
menu_id["id"] = "id";
menu_id["value"] = menu_id["name"];
menu_id["placeholder"] = "Unique ID from the M3U file";
menu_id["onclick"] = "javascript: toggleMenu(this);";
menu_id["class"] = "menu-notActive";
var menu_xmltv = new Object();
menu_xmltv["_menuType"] = "inputArray";
menu_xmltv["_element"] = "LI";
menu_xmltv["_configKey"] = "files.xmltv";
menu_xmltv["_text"] = "XMLTV";
menu_xmltv["_icon"] = "img/xmltv.png";
menu_xmltv["_headline"] = "XMLTV files: Local or remote";
menu_xmltv["_usage"] = "<b>Info:</b><br>Availability: File availability in percent<br>Channels: Number of channels in the file<br>Programs: Number of EPG data<br><br><b>Usage:</b><br>Remote XMLTV file: http://your.epg.provider.com/guide.xml<br>Local XMLTV file: /path/to/guide.xml"
menu_xmltv["name"] = "xmltv";
menu_xmltv["id"] = "xmltv";
menu_xmltv["value"] = menu_xmltv["name"];
menu_xmltv["placeholder"] = "XMLTV File: local or remote";
menu_xmltv["onclick"] = "javascript: toggleMenu(this);";
menu_xmltv["class"] = "menu-notActive";
menu_mapping = new Object();
menu_mapping["_element"] = "LI";
menu_mapping["_text"] = "Mapping";
menu_mapping["_icon"] = "img/mapping.png";
menu_mapping["_configKey"] = "mapping";
menu_mapping["_headline"] = "XMLTV assignment and sorting of channels";
menu_mapping["id"] = "mapping";
menu_mapping["onclick"] = "javascript: toggleMenu(this);";
menu_mapping["class"] = "menu-notActive phone";
menu_users = new Object();
menu_users["_element"] = "LI";
menu_users["_text"] = "Users";
menu_users["_icon"] = "img/users.png";
menu_users["_configKey"] = "users";
menu_users["_headline"] = "Administration of users and permissions";
menu_users["id"] = "users";
menu_users["onclick"] = "javascript: toggleMenu(this);";
menu_users["class"] = "menu-notActive";
menu_users["_usage"] = "<b>Authorization groups:</b><br>WEB: Users can log in to the web interface<br>PMS: Programs like Plex can access the channel list. Login via DVR IP: username:password@xteve.ip:port<br>M3U: Allows clients to download the M3U playlist.<br>XML: Allows clients to download the XMLTV file.<br>API: Allows clients to use the API interface.<br><br>!!! For PMS authentication, only the following special characters are valid: !$()=.,-:;<br><br>The individual authentication groups can be activated / deactivated in the settings menu."
menu_settings = new Object();
menu_settings["_element"] = "LI";
menu_settings["_text"] = "Settings";
menu_settings["_icon"] = "img/settings.png";
menu_settings["_configKey"] = "settings";
menu_settings["_headline"] = "Settings";
menu_settings["_subMenu"] = "701,702,703,704,705,706,707,708,799,710,711,712,713,714";
menu_settings["id"] = "settings";
menu_settings["onclick"] = "javascript: toggleMenu(this);";
menu_settings["class"] = "menu-notActive";
menu_log = new Object();
menu_log["_element"] = "LI";
menu_log["_text"] = "Log";
menu_log["_icon"] = "img/log.png";
menu_log["_headline"] = "Log";
menu_log["_configKey"] = "log";
menu_log["id"] = "log";
menu_log["onclick"] = "javascript: toggleMenu(this);";
menu_log["class"] = "menu-notActive";
menu_logout = new Object();
menu_logout["_element"] = "LI";
menu_logout["_text"] = "Logout";
menu_logout["_icon"] = "img/logout.png";
menu_logout["id"] = "logout";
menu_logout["onclick"] = "javascript: logout();";
menu_logout["class"] = "menu-notActive";
var menu_schedule = new Object();
menu_schedule["_menuType"] = "inputArray";
menu_schedule["_element"] = "LI";
menu_schedule["_configKey"] = "update";
menu_schedule["_text"] = "Schedule";
menu_schedule["_icon"] = "img/schedule.png";
menu_schedule["_headline"] = "Schedule for updating M3U, XMLTV files and creating a local backup";
menu_schedule["_usage"] = "<b>Usage:</b><br>0815 = 8:15 am<br>1930 = 7:30 pm"
menu_schedule["name"] = "update";
menu_schedule["id"] = "update";
menu_schedule["value"] = menu_id["name"];
menu_schedule["placeholder"]= "time of day (24-hour clock)";
menu_schedule["onclick"] = "javascript: toggleMenu(this);";
menu_schedule["class"] = "menu-notActive";
var menu_filesUpdate = new Object();
menu_filesUpdate["_element"] = "LI";
menu_filesUpdate["_menuType"] = "checkbox";
menu_filesUpdate["_configKey"] = "files.update";
menu_filesUpdate["_label"] = "Update the provider files at system startup";
menu_filesUpdate["_headline"] = "Update the provider files at system startup";
menu_filesUpdate["_usage"] = "Playlists and XMLTV files are updated by xTeVe at system startup."
menu_filesUpdate["name"] = "files.update";
menu_filesUpdate["id"] = "files.update";
menu_filesUpdate["value"] = menu_filesUpdate["name"];
menu_filesUpdate["onclick"] = "javascript: toggleMenu(this);";
menu_filesUpdate["class"] = "menu-notActive";
var menu_tuner = new Object();
menu_tuner["_element"] = "LI";
menu_tuner["_menuType"] = "select";
menu_tuner["_configKey"] = "tuner";
menu_tuner["_label"] = "Available tuners";
menu_tuner["_text"] = "Tuner";
menu_tuner["_icon"] = "img/tuner.png";
menu_tuner["_headline"] = "Number of tuners";
menu_tuner["_usage"] = "This setting is only used by Plex and Emby.<br>The number of concurrent streams allowed by the IPTV provider.<br>After a change, xTeVe must be delete in the PMS DVR settings and set up again."
menu_tuner["name"] = "tuner";
menu_tuner["id"] = "tuner";
menu_tuner["value"] = menu_tuner["name"];
menu_tuner["placeholder"] = "Number of tuners";
menu_tuner["onclick"] = "javascript: toggleMenu(this);";
menu_tuner["class"] = "menu-notActive";
var optionValues = new Array();
for (var i = 1; i <= 100; i++) {
optionValues.push(i)
}
menu_tuner["_optionValues"] = optionValues;
var menu_epg = new Object();
menu_epg["_element"] = "LI";
menu_epg["_menuType"] = "select";
menu_epg["_configKey"] = "epgSource";
menu_epg["_label"] = "Selection of the EPG source";
menu_epg["_text"] = "EPG source";
menu_epg["_headline"] = "Selection of the EPG source";
menu_epg["_usage"] = "PMS: Use EPG data from Plex or Emby.<br>XEPG: Use of external EPG data (XMLTV).<br> Several XMLTV sources possible.<br> Allows editing and order channels.<br> M3U / XMLTV export (HTTP link for IPTV apps)."
menu_epg["name"] = "epgSource";
menu_epg["id"] = "epgSource";
menu_epg["value"] = menu_epg["name"];
menu_epg["placeholder"] = "EPG source";
menu_epg["onclick"] = "javascript: toggleMenu(this);";
menu_epg["class"] = "menu-notActive";
menu_epg["_optionValues"] = new Array("PMS", "XEPG");
var menu_xepg = new Object();
menu_xepg["_element"] = "LI";
menu_xepg["_menuType"] = "checkbox";
menu_xepg["_configKey"] = "xteveAutoUpdate";
menu_xepg["_label"] = "Automatic update of xTeVe";
menu_xepg["_headline"] = "Automatic update of xTeVe";
menu_xepg["_usage"] = "If a new version of xTeVe is available, it will be automatically installed."
menu_xepg["name"] = "xteveAutoUpdate";
menu_xepg["id"] = "xteveAutoUpdate";
menu_xepg["value"] = menu_xepg["name"];
menu_xepg["onclick"] = "javascript: toggleMenu(this);";
menu_xepg["class"] = "menu-notActive";
var menu_autoBackupPath = new Object();
menu_autoBackupPath["_element"] = "LI";
menu_autoBackupPath["_menuType"] = "singleInput";
menu_autoBackupPath["_configKey"] = "backup.path";
menu_autoBackupPath["_label"] = "Location for automatic backups";
menu_autoBackupPath["_headline"] = "Location for automatic backups";
menu_autoBackupPath["_usage"] = "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."
menu_autoBackupPath["name"] = "backup.path";
menu_autoBackupPath["id"] = "backup.path";
menu_autoBackupPath["value"] = menu_autoBackupPath["name"];
menu_autoBackupPath["onclick"] = "javascript: toggleMenu(this);";
menu_autoBackupPath["class"] = "menu-notActive";
var menu_autoBackupKeep = new Object();
menu_autoBackupKeep["_element"] = "LI";
menu_autoBackupKeep["_menuType"] = "select";
menu_autoBackupKeep["_configKey"] = "backup.keep";
menu_autoBackupKeep["_text"] = "Keep";
menu_autoBackupKeep["_label"] = "Number of backups to keep";
menu_autoBackupKeep["_headline"] = "Number of backups to keep";
menu_autoBackupKeep["_usage"] = ""
menu_autoBackupKeep["name"] = "backup.keep";
menu_autoBackupKeep["id"] = "backup.keep";
menu_autoBackupKeep["value"] = menu_autoBackupKeep["name"];
menu_autoBackupKeep["onclick"] = "javascript: toggleMenu(this);";
menu_autoBackupKeep["class"] = "menu-notActive";
var optionValues = new Array(5, 10, 20, 30, 40, 50);
menu_autoBackupKeep["_optionValues"] = optionValues;
var menu_buffer = new Object();
menu_buffer["_element"] = "LI";
menu_buffer["_menuType"] = "checkbox";
menu_buffer["_configKey"] = "buffer";
menu_buffer["_label"] = "Stream buffering [Experimental]";
menu_buffer["_headline"] = "Stream buffering [Experimental]";
menu_buffer["_usage"] = "With activated buffer, streams can be played and recorded more fluently.<br>The stream is passed from xTeVe to Plex / Emby"
menu_buffer["name"] = "buffer";
menu_buffer["id"] = "buffer";
menu_buffer["value"] = menu_buffer["name"];
menu_buffer["onclick"] = "javascript: toggleMenu(this);";
menu_buffer["class"] = "menu-notActive";
var menu_api = new Object();
menu_api["_element"] = "LI";
menu_api["_menuType"] = "checkbox";
menu_api["_configKey"] = "api";
menu_api["_label"] = "API interface";
menu_api["_headline"] = "API interface";
menu_api["_usage"] = 'Via API interface it is possible to send commands to xTeVe. API documentation is available <a href="https://xteve.de?scroll=api">here</a> '
//menu_api["_usage"] = 'Via API interface it is possible to send commands to xTeVe. API documentation is available <a href="http://localhost:1313?scroll=api">here</a> '
menu_api["name"] = "api";
menu_api["id"] = "api";
menu_api["value"] = menu_api["name"];
menu_api["onclick"] = "javascript: toggleMenu(this);";
menu_api["class"] = "menu-notActive";
var menu_authenticationWeb = new Object();
menu_authenticationWeb["_element"] = "LI";
menu_authenticationWeb["_menuType"] = "checkbox";
menu_authenticationWeb["_configKey"] = "authentication.web";
menu_authenticationWeb["_label"] = "User authentication";
menu_authenticationWeb["_headline"] = "User authentication";
menu_authenticationWeb["_usage"] = "Access to xTeVe requires authentication."
menu_authenticationWeb["name"] = "authentication.web";
menu_authenticationWeb["id"] = "authentication.web";
menu_authenticationWeb["value"] = menu_authenticationWeb["name"];
menu_authenticationWeb["onclick"] = "javascript: toggleMenu(this);";
menu_authenticationWeb["class"] = "menu-notActive";
var menu_authenticationPms = new Object();
menu_authenticationPms["_element"] = "LI";
menu_authenticationPms["_menuType"] = "checkbox";
menu_authenticationPms["_configKey"] = "authentication.pms";
menu_authenticationPms["_label"] = "Plex authentication.";
menu_authenticationPms["_headline"] = "Plex authentication.";
menu_authenticationPms["_usage"] = "Plex requests are only possible with authentication.<br>Warning!!! After activating this function xTeVe must be delete in the PMS DVR settings and set up again."
menu_authenticationPms["name"] = "authentication.pms";
menu_authenticationPms["id"] = "authentication.pms";
menu_authenticationPms["value"] = menu_authenticationPms["name"];
menu_authenticationPms["onclick"] = "javascript: toggleMenu(this);";
menu_authenticationPms["class"] = "menu-notActive";
var menu_authenticationM3u = new Object();
menu_authenticationM3u["_element"] = "LI";
menu_authenticationM3u["_menuType"] = "checkbox";
menu_authenticationM3u["_configKey"] = "authentication.m3u";
menu_authenticationM3u["_label"] = "M3U authentication.";
menu_authenticationM3u["_headline"] = "M3U authentication.";
menu_authenticationM3u["_usage"] = "Downloading the M3U file via an HTTP request is only possible with authentication."
menu_authenticationM3u["name"] = "authentication.m3u";
menu_authenticationM3u["id"] = "authentication.m3u";
menu_authenticationM3u["value"] = menu_authenticationM3u["name"];
menu_authenticationM3u["onclick"] = "javascript: toggleMenu(this);";
menu_authenticationM3u["class"] = "menu-notActive";
var menu_authenticationXml = new Object();
menu_authenticationXml["_element"] = "LI";
menu_authenticationXml["_menuType"] = "checkbox";
menu_authenticationXml["_configKey"] = "authentication.xml";
menu_authenticationXml["_label"] = "XEPG authentication";
menu_authenticationXml["_headline"] = "XEPG authentication";
menu_authenticationXml["_usage"] = "Downloading the XEPG (XMLTV) file via an HTTP request is only possible with authentication."
menu_authenticationXml["name"] = "authentication.xml";
menu_authenticationXml["id"] = "authentication.xml";
menu_authenticationXml["value"] = menu_authenticationXml["name"];
menu_authenticationXml["onclick"] = "javascript: toggleMenu(this);";
menu_authenticationXml["class"] = "menu-notActive";
var menu_authenticationApi = new Object();
menu_authenticationApi["_element"] = "LI";
menu_authenticationApi["_menuType"] = "checkbox";
menu_authenticationApi["_configKey"] = "authentication.api";
menu_authenticationApi["_label"] = "API authentication";
menu_authenticationApi["_headline"] = "API authentication";
menu_authenticationApi["_usage"] = "Access to the API interface is only possible with authentication."
menu_authenticationApi["name"] = "authentication.api";
menu_authenticationApi["id"] = "authentication.api";
menu_authenticationApi["value"] = menu_authenticationApi["name"];
menu_authenticationApi["onclick"] = "javascript: toggleMenu(this);";
menu_authenticationApi["class"] = "menu-notActive";
// Main menu
menu[10] = menu_m3u;
switch(config["epgSource"]) {
case "PMS":
menu[20] = menu_id;
break;
case "XMLTV":
menu[40] = menu_xmltv;
break;
case "XEPG":
menu[40] = menu_xmltv;
menu[50] = menu_mapping;
break;
}
menu[30] = menu_filter;
if (config["authentication.web"] == true) {
menu[60] = menu_users;
}
menu[70] = menu_settings;
menu[80] = menu_log;
if (config["authentication.web"] == true) {
menu[100] = menu_logout;
}
// Sub-Menu
subMenu[701] = menu_schedule;
subMenu[702] = menu_filesUpdate;
subMenu[703] = menu_tuner;
subMenu[704] = menu_epg;
subMenu[705] = menu_xepg;
subMenu[706] = menu_autoBackupPath;
subMenu[707] = menu_autoBackupKeep;
subMenu[708] = menu_buffer;
subMenu[710] = menu_authenticationWeb;
if (config["authentication.web"] == true) {
subMenu[711] = menu_authenticationPms;
subMenu[712] = menu_authenticationM3u;
subMenu[713] = menu_authenticationXml;
subMenu[714] = menu_authenticationApi;
}
subMenu[799] = menu_api;
return
}
function createMenu() {
showElement("popup", false);
//console.log(config);
setMenuItem();
var menuItems = getObjKeys(menu)
var nav = document.getElementsByTagName("NAV")[0];
nav.innerHTML = "";
var newItem = new Object();
for (var i = 0; i < menuItems.length; i++) {
var newItem = menu[menuItems[i]];
newItem["id"] = menuItems[i];
switch(newItem.hasOwnProperty("_icon")) {
case true:
var itemText = newItem["_text"];
delete newItem["_text"]
nav.appendChild(createElement(newItem));
newItem["_text"] = itemText;
var newIcon = new Object();
newIcon["_element"] = "IMG";
newIcon["src"] = newItem["_icon"];
var currentElement = document.getElementById(menuItems[i]);
currentElement.appendChild(createElement(newIcon));
var text = new Object();
text["_element"] = "P"
text["_text"] = itemText;
text["class"] = "nav-text"
currentElement.appendChild(createElement(text));
break;
default:
nav.appendChild(createElement(newIcon));
break;
}
}
if (activeMenu != undefined) {
//console.log(activeMenu);
toggleMenu(activeMenu);
}
return
}
function toggleMenu(elm) {
//showStreams(false);
clearInterval(logInterval)
activeMenu = elm;
var item = menu[elm.id]
var div = document.getElementById("settings");
div.innerHTML = "";
// Set Headline
var headline = new Object();
headline["_element"] = "H4";
headline["_text"] = item["_headline"];
div.appendChild(createElement(headline));
// Sub-Menu
if (item.hasOwnProperty("_subMenu") == true) {
openSubMenu(item);
return
}
// Mapping, Users, Log, Files
switch(item["_configKey"]) {
case "mapping": openMappingEditor(item); return; break;
case "users": openUsers(item); return; break;
case "log": showLog(item); return; break;
case "files.m3u": openFiles(item, "m3u"); return; break;
case "files.xmltv": openFiles(item, "xmltv"); return; break;
case "filter": showStreams(true); break;
}
var newHR = new Object();
newHR["_element"] = "HR"
div.appendChild(createElement(newHR));
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
//newEntry["class"] = "save";
newEntry["value"] = "Save";
newEntry["onclick"] = "saveData2('settings')"
div.appendChild(createElement(newEntry));
var newWrapper = new Object();
newWrapper["_element"] = "DIV";
newWrapper["id"] = "box-wrapper";
div.appendChild(createElement(newWrapper));
div = div.lastChild;
div.appendChild(createMenuItem(item))
// usage Info
switch(menu[activeMenu.id].hasOwnProperty("_usage")) {
case true:
var usageItem = new Object();
usageItem["_element"] = "PRE"
usageItem["_text"] = menu[activeMenu.id]["_usage"];
div.appendChild(createElement(usageItem));
}
calculateWrapperHeight();
}
function createMenuItem(item) {
var element = document.createElement("DIV");
switch(item["_menuType"]) {
case "inputArray":
if (config.hasOwnProperty(item["_configKey"]) == true) {
var value = config[item["_configKey"]];
} else {
var value = new Array();
}
for (var i = 0; i < value.length; i++) {
var newEntry = new Object();
newEntry = item
delete newEntry["onclick"];
newEntry["_element"] = "INPUT";
newEntry["value"] = value[i];
newEntry["type"] = "search";
newEntry["data-menutype"] = item["_menuType"];
newEntry["data-menukey"] = item["_configKey"];
element.appendChild(createElement(newEntry));
}
// New entry for array
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "search";
newEntry["name"] = item["name"];
newEntry["placeholder"] = item["placeholder"];
newEntry["value"] = "";
newEntry["data-menutype"] = item["_menuType"];
newEntry["data-menukey"] = item["_configKey"];
element.appendChild(createElement(newEntry));
break;
case "singleInput":
var value = config[item["_configKey"]];
if (value == undefined) {
value = "";
}
var newEntry = new Object();
newEntry = item;
delete newEntry["onclick"];
newEntry["_element"] = "INPUT";
newEntry["value"] = value;
newEntry["type"] = "search";
newEntry["data-menutype"] = item["_menuType"];
newEntry["data-menukey"] = item["_configKey"];
element.appendChild(createElement(newEntry));
break;
case "checkbox":
var value = config[item["_configKey"]];
if (value == undefined) {
value = false;
}
var newEntry = new Object();
newEntry = item;
delete newEntry["onclick"];
newEntry["_element"] = "INPUT";
newEntry["value"] = value;
newEntry["type"] = "checkbox";
newEntry["data-menutype"] = item["_menuType"];
newEntry["data-menukey"] = item["_configKey"];
element.appendChild(createElement(newEntry));
element.getElementsByTagName("INPUT")[0].checked = value;
break;
case "select":
var value = config[item["_configKey"]];
var newEntry = new Object();
newEntry = item;
delete newEntry["onclick"]
newEntry["_element"] = "SELECT";
element.appendChild(createElement(newEntry));
var selectElement = element.getElementsByTagName("SELECT")[0];
var values = item["_optionValues"];
for (var i = 0; i < values.length; i++) {
var newEntry = new Object;
newEntry["_element"] = "OPTION";
newEntry["_text"] = item["_text"] + ": " + values[i];
newEntry["value"] = values[i];
selectElement.appendChild(createElement(newEntry));
}
selectElement.value = value;
break;
}
return element;
}
function openSubMenu(item) {
var entrys = item["_subMenu"].split(",");
var div = document.getElementById("settings");
var newHR = new Object();
newHR["_element"] = "HR"
div.appendChild(createElement(newHR));
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
//newEntry["class"] = "save";
newEntry["value"] = "Save";
newEntry["onclick"] = "saveData2('settings')"
div.appendChild(createElement(newEntry));
if (item["_configKey"] == "settings") {
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
//newEntry["class"] = "save";
newEntry["value"] = "Backup";
newEntry["onclick"] = "xteveBackup()"
div.appendChild(createElement(newEntry));
}
if (item["_configKey"] == "settings") {
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
//newEntry["class"] = "save";
newEntry["value"] = "Restore";
newEntry["onclick"] = "xteveRestore(this)"
div.appendChild(createElement(newEntry));
}
var newWrapper = new Object();
newWrapper["_element"] = "DIV";
newWrapper["id"] = "box-wrapper";
div.appendChild(createElement(newWrapper));
div = div.lastChild;
for (var i = 0; i < entrys.length; i++) {
var item = subMenu[entrys[i]];
if (item == undefined) {
break;
}
var container = new Object();
container["_element"] = "DIV";
div.appendChild(createElement(container));
var divContainer = div.lastChild;
var headline = new Object();
headline["_element"] = "H5";
headline["_text"] = item["_headline"];
divContainer.appendChild(createElement(headline));
divContainer.appendChild(createMenuItem(item))
switch(item.hasOwnProperty("_usage")) {
case true:
var usageItem = new Object();
usageItem["_element"] = "PRE"
usageItem["_text"] = item["_usage"];
divContainer.appendChild(createElement(usageItem));
}
var hr = new Object();
hr["_element"] = "HR";
divContainer.appendChild(createElement(hr));
}
calculateWrapperHeight();
return
}
function saveData2(elm) {
var div = document.getElementById(elm);
var inputs = div.getElementsByTagName("INPUT");
var selects = div.getElementsByTagName("SELECT");
var value, configKey;
var data = new Object();
var valueArr = new Array();
var newData = false;
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].type != "button") {
var menuType = inputs[i].getAttribute("data-menutype");
//console.log(menuType);
switch(menuType) {
case "singleInput":
value = inputs[i].value;
if (value == "" || value == undefined) {
data = new Object();
data["delete"] = inputs[i].name
newData = true;
} else {
newData = true;
data[inputs[i].name] = value;
console.log(data);
}
break;
case "inputArray":
value = inputs[i].value;
if (value != "" && value != undefined) {
newData = true;
valueArr.push(value)
data[inputs[i].name] = valueArr;
configKey = inputs[i].name;
}
break;
case "checkbox":
value = inputs[i].checked
data[inputs[i].name] = value;
}
}
}
// Delete config key
if (valueArr.length == 0 && newData == false) {
newData = true;
data = new Object();
data["delete"] = configKey;
}
for (var i = 0; i < selects.length; i++) {
var value = selects[i].options[selects[i].selectedIndex].value;
switch(isNaN(value)) {
case false: value = parseInt(value); break;
}
data[selects[i].name] = value;
newData = true;
}
//console.log(data, newData);
if (newData == true) {
data["cmd"] = "saveConfig";
if (!data.hasOwnProperty('filter')) {
data["filter"] = config["filter"]
}
var settings = new Object();
settings["cmd"] = data["cmd"];
settings["settings"] = data;
console.log(settings);
xTeVe(settings);
}
}

1747
html/js/menu_ts.js Normal file

File diff suppressed because it is too large Load Diff

105
html/js/network_ts.js Normal file
View File

@@ -0,0 +1,105 @@
var Server = /** @class */ (function () {
function Server(cmd) {
this.cmd = cmd;
}
Server.prototype.request = function (data) {
if (SERVER_CONNECTION == true) {
return;
}
SERVER_CONNECTION = true;
console.log(data);
if (this.cmd != "updateLog") {
showElement("loading", true);
UNDO = new Object();
}
switch (window.location.protocol) {
case "http:":
this.protocol = "ws://";
break;
case "https://":
this.protocol = "wss://";
break;
}
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token");
data["cmd"] = this.cmd;
var ws = new WebSocket(url);
ws.onopen = function () {
WS_AVAILABLE = true;
console.log("REQUEST (JS):");
console.log(data);
console.log("REQUEST: (JSON)");
console.log(JSON.stringify(data));
this.send(JSON.stringify(data));
};
ws.onerror = function (e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.");
SERVER_CONNECTION = false;
if (WS_AVAILABLE == false) {
alert("No websocket connection to xTeVe could be established. Check your network configuration.");
}
};
ws.onmessage = function (e) {
SERVER_CONNECTION = false;
showElement("loading", false);
console.log("RESPONSE:");
var response = JSON.parse(e.data);
console.log(response);
if (response.hasOwnProperty("token")) {
document.cookie = "Token=" + response["token"];
}
if (response["status"] == false) {
alert(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 (data["cmd"]) {
case "updateLog":
SERVER["log"] = response["log"];
if (document.getElementById("content_log")) {
showLogs(false);
}
return;
break;
default:
SERVER = new Object();
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")) {
alert(response["alert"]);
}
if (response.hasOwnProperty("reload")) {
location.reload();
}
if (response.hasOwnProperty("wizard")) {
createLayout();
configurationWizard[response["wizard"]].createWizard();
return;
}
createLayout();
};
};
return Server;
}());
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2)
return parts.pop().split(";").shift();
}

442
html/js/settings_ts.js Normal file
View File

@@ -0,0 +1,442 @@
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var SettingsCategory = /** @class */ (function () {
function SettingsCategory() {
this.DocumentID = "content_settings";
}
SettingsCategory.prototype.createCategoryHeadline = function (value) {
var element = document.createElement("H4");
element.innerHTML = value;
return element;
};
SettingsCategory.prototype.createHR = function () {
var element = document.createElement("HR");
return element;
};
SettingsCategory.prototype.createSettings = function (settingsKey) {
var setting = document.createElement("TR");
var content = new PopupContent();
var data = SERVER["settings"][settingsKey];
switch (settingsKey) {
// Texteingaben
case "update":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.update.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "update", data.toString());
input.setAttribute("placeholder", "{{.settings.update.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "backup.path":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.backupPath.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "backup.path", data);
input.setAttribute("placeholder", "{{.settings.backupPath.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "temp.path":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.tempPath.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "temp.path", data);
input.setAttribute("placeholder", "{{.settings.tmpPath.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "user.agent":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.userAgent.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "user.agent", data);
input.setAttribute("placeholder", "{{.settings.userAgent.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "buffer.timeout":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "buffer.timeout", data);
input.setAttribute("placeholder", "{{.settings.bufferTimeout.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
// Checkboxen
case "authentication.web":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.authenticationWEB.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "authentication.pms":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.authenticationPMS.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "authentication.m3u":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.authenticationM3U.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "authentication.xml":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.authenticationXML.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "authentication.api":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.authenticationAPI.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "files.update":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.filesUpdate.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "cache.images":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.cacheImages.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "xepg.replace.missing.images":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.replaceEmptyImages.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "xteveAutoUpdate":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.xteveAutoUpdate.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "buffer":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "api":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.api.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
// Select
case "tuner":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":";
var tdRight = document.createElement("TD");
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, data, settingsKey);
select.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(select);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "epgSource":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.epgSource.title}}" + ":";
var tdRight = document.createElement("TD");
var text = ["PMS", "XEPG"];
var values = ["PMS", "XEPG"];
var select = content.createSelect(text, values, data, settingsKey);
select.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(select);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "backup.keep":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.backupKeep.title}}" + ":";
var tdRight = document.createElement("TD");
var text = ["5", "10", "20", "30", "40", "50"];
var values = ["5", "10", "20", "30", "40", "50"];
var select = content.createSelect(text, values, data, settingsKey);
select.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(select);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "buffer.size.kb":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.bufferSize.title}}" + ":";
var tdRight = document.createElement("TD");
var text = ["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"];
var values = ["512", "1024", "2048", "3072", "4096", "5120", "6144", "7168", "8192"];
var select = content.createSelect(text, values, data, settingsKey);
select.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(select);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
}
return setting;
};
SettingsCategory.prototype.createDescription = function (settingsKey) {
var description = document.createElement("TR");
var text;
switch (settingsKey) {
case "authentication.web":
text = "{{.settings.authenticationWEB.description}}";
break;
case "authentication.m3u":
text = "{{.settings.authenticationM3U.description}}";
break;
case "authentication.pms":
text = "{{.settings.authenticationPMS.description}}";
break;
case "authentication.xml":
text = "{{.settings.authenticationXML.description}}";
break;
case "authentication.api":
if (SERVER["settings"]["authentication.web"] == true) {
text = "{{.settings.authenticationAPI.description}}";
}
break;
case "xteveAutoUpdate":
text = "{{.settings.xteveAutoUpdate.description}}";
break;
case "backup.keep":
text = "{{.settings.backupKeep.description}}";
break;
case "backup.path":
text = "{{.settings.backupPath.description}}";
break;
case "temp.path":
text = "{{.settings.tempPath.description}}";
break;
case "buffer":
text = "{{.settings.streamBuffering.description}}";
break;
case "buffer.size.kb":
text = "{{.settings.bufferSize.description}}";
break;
case "buffer.timeout":
text = "{{.settings.bufferTimeout.description}}";
break;
case "user.agent":
text = "{{.settings.userAgent.description}}";
break;
case "epgSource":
text = "{{.settings.epgSource.description}}";
break;
case "tuner":
text = "{{.settings.tuner.description}}";
break;
case "update":
text = "{{.settings.update.description}}";
break;
case "api":
text = "{{.settings.api.description}}";
break;
case "files.update":
text = "{{.settings.filesUpdate.description}}";
break;
case "cache.images":
text = "{{.settings.cacheImages.description}}";
break;
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}";
break;
default:
text = "";
break;
}
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;
};
return SettingsCategory;
}());
var SettingsCategoryItem = /** @class */ (function (_super) {
__extends(SettingsCategoryItem, _super);
function SettingsCategoryItem(headline, settingsKeys) {
var _this = _super.call(this) || this;
_this.headline = headline;
_this.settingsKeys = settingsKeys;
return _this;
}
SettingsCategoryItem.prototype.createCategory = function () {
var _this = this;
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(function (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());
};
return SettingsCategoryItem;
}(SettingsCategory));
function showSettings() {
console.log("SETTINGS");
for (var 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 (var 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":
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);
}

341
html/js/users.js Normal file
View File

@@ -0,0 +1,341 @@
function openUsers(elm) {
colomnSort = 0;
var newDiv = document.getElementById("settings");
var newEntry = new Object();
newEntry["_element"] = "HR";
newDiv.appendChild(createElement(newEntry));
var newEntry = new Object();
newEntry["_element"] = "INPUT";
newEntry["type"] = "button";
newEntry["class"] = "button";
newEntry["value"] = "New";
newEntry["onclick"] = "userDetail(0)";
newDiv.appendChild(createElement(newEntry));
var div = document.getElementById("settings");
// Build table
var newTable = new Object();
newTable["_element"] = "TABLE";
newTable["id"] = "id_mapping";
newTable["class"] = "table-mapping";
div.appendChild(createElement(newTable));
setTimeout(function(){
createUsersTable();
}, 10);
}
function createUsersTable() {
var table = document.getElementById("id_mapping");
table.innerHTML = "";
var newTR = new Object();
newTR["_element"] = "TR";
newTR["class"] = "table-mapping-header";
table.appendChild(createElement(newTR));
var tr = table.lastChild;
var trHeadlines = new Array("Username", "Password", "WEB", "PMS", "M3U", "XML", "API")
for (var i = 0; i < trHeadlines.length; i++) {
var newTD = new Object();
newTD["_element"] = "TD";
newTD["_text"] = trHeadlines[i];
tr.appendChild(createElement(newTD));
}
// Sort users
var userIds = getObjKeys(users);
var userObj = new Object();
for (var i = 0; i < userIds.length; i++) {
var username = users[userIds[i]]["data"]["username"];
userObj[username] = userIds[i];
}
var allUsers = getObjKeys(userObj);
allUsers.sort();
// --
for (var i = 0; i < allUsers.length; i++) {
var table = document.getElementById("id_mapping");
var userID = userObj[allUsers[i]];
var username = allUsers[i];
var item = users[userID]["data"];
// Create TR
var newTR = new Object();
newTR["_element"] = "TR";
newTR["class"] = "";
newTR["id"] = userID;
newTR["onclick"] = 'javascript: userDetail("' + userID + '");';
table.appendChild(createElement(newTR));
var tr = table.lastChild;
// Create username TD
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = username;
createNewTD(newTD, tr);
// Create password TD
var newTD = new Object();
newTD["_element"] = "P";
newTD["_text"] = ".....";
createNewTD(newTD, tr);
// Create web access
var newTD = new Object();
newTD["_element"] = "P";
switch(item["authentication.web"]){
case true: newTD["_text"] = "✓"; break;
default: newTD["_text"] = "-"; break;
}
createNewTD(newTD, tr);
// Create PMS access
var newTD = new Object();
newTD["_element"] = "P";
switch(item["authentication.pms"]){
case true: newTD["_text"] = "✓"; break;
default: newTD["_text"] = "-"; break;
}
createNewTD(newTD, tr);
// Create M3U access
var newTD = new Object();
newTD["_element"] = "P";
switch(item["authentication.m3u"]){
case true: newTD["_text"] = "✓"; break;
default: newTD["_text"] = "-"; break;
}
createNewTD(newTD, tr);
// Create XMLTV access
var newTD = new Object();
newTD["_element"] = "P";
switch(item["authentication.xml"]){
case true: newTD["_text"] = "✓"; break;
default: newTD["_text"] = "-"; break;
}
createNewTD(newTD, tr);
// Create API access
var newTD = new Object();
newTD["_element"] = "P";
switch(item["authentication.api"]){
case true: newTD["_text"] = "✓"; break;
default: newTD["_text"] = "-"; break;
}
createNewTD(newTD, tr);
}
// usage Info
var div = document.getElementById("settings");
switch(menu[activeMenu.id].hasOwnProperty("_usage")) {
case true:
var usageItem = new Object();
usageItem["_element"] = "PRE"
usageItem["_text"] = menu[activeMenu.id]["_usage"];
var newHR = new Object();
newHR["_element"] = "HR"
div.appendChild(createElement(newHR));
div.appendChild(createElement(usageItem));
}
sortTable(0);
}
function userDetail(userID) {
showPopUpElement('user-detail');
setTimeout(function(){
showElement("popup", true);
}, 10);
var defaultUser;
document.getElementById("saveUserDetail").setAttribute("onclick", 'javascript: saveUserDetail("' + userID + '", false)');
document.getElementById("deleteUserDetail").setAttribute("onclick", 'javascript: saveUserDetail("' + userID + '", true)');
var data = new Object();
switch(userID) {
case 0: // New User
data["username"] = "";
data["authentication.web"] = false;
data["authentication.pms"] = true;
data["authentication.xml"] = true;
data["authentication.m3u"] = false;
data["authentication.api"] = false;
data["defaultUser"] = false;
setTimeout(function(){
showElement("deleteUserDetail", false)
}, 1);
break;
default:
data = users[userID]["data"];
showElement("deleteUserDetail", true)
document.getElementById("deleteUserDetail").className = "delete";
break
}
var username = data["username"];
data["password"] = "";
data["confirm"] = "";
var keys = getObjKeys(data);
defaultUser = data["defaultUser"];
if (data.hasOwnProperty("defaultUser")) {
defaultUser = JSON.parse(data["defaultUser"]);
}
for (var i = 0; i < keys.length; i++) {
if(document.getElementById(keys[i])){
var td = document.getElementById(keys[i])
} else {
var td = undefined;
}
var newItem = new Object();
newItem["_element"] = "INPUT";
newItem["value"] = data[keys[i]];
newItem["name"] = keys[i];
switch(keys[i].indexOf("authentication")) {
case -1:
if (keys[i] == "password" || keys[i] == "confirm") {
newItem["type"] = "password";
} else {
newItem["type"] = "text";
}
break;
default:
newItem["type"] = "checkbox";
if (keys[i] == "authentication.web" && defaultUser == true) {
newItem["onclick"] = "return false";
}
if (data[keys[i]] == true) {
newItem["checked"] = data[keys[i]];
}
break;
}
switch(keys[i]) {
case "defaultUser":
//if (data[keys[i]] == true) {
newItem["type"] = "hidden";
//}
}
if (td != undefined) {
td.innerHTML = "";
var element = createNewElement(newItem)
//console.log(element);
td.appendChild(element);
}
}
if (defaultUser == true) {
showElement("deleteUserDetail", false)
} else {
showElement("deleteUserDetail", true)
document.getElementById("deleteUserDetail").className = "delete";
}
}
function saveUserDetail(userID, deleteUser) {
var inputs = document.getElementById("user-detail-table").getElementsByTagName("INPUT");
var newUserData = new Object();
for (var i = 0; i < inputs.length; i++) {
switch(inputs[i].type) {
case "checkbox": newUserData[inputs[i].name] = inputs[i].checked; break;
default: newUserData[inputs[i].name] = inputs[i].value; break;
}
if (inputs["username"].value.length == 0) {
inputs["username"].style.border = "solid 1px red";
return;
}
switch(userID) {
case "0":
if (inputs["password"].value.length == 0) {
console.log(inputs["password"].value.length);
inputs["password"].style.border = "solid 1px red";
return
}
break;
}
if (inputs["password"].value.length > 0) {
if (inputs["password"].value != inputs["confirm"].value) {
inputs["password"].style.border = "solid 1px red";
inputs["confirm"].style.border = "solid 1px red";
return;
}
}
}
var data = new Object();
switch(userID) {
case "0":
//data = newUserData
data["userData"] = newUserData
data["cmd"] = "saveNewUser"; break;
default:
var thisUser = new Object();
if (deleteUser == true) {
if (confirm('Delete the selected user?')) {
data["deleteUser"] = true;
} else {
showElement("popup", false);
return
}
}
thisUser[userID] = newUserData;
data["userData"] = thisUser;
data["cmd"] = "saveUserData"; break;
}
xTeVe(data);
//createUsersTable()
showElement("popup", false);
}

419
html/lang/en.json Normal file
View File

@@ -0,0 +1,419 @@
{
"mainMenu": {
"item":{
"playlist": "Playlist",
"pmsID": "PMS ID",
"filter": "Filter",
"xmltv": "XMLTV",
"mapping": "Mapping",
"users": "Users",
"settings": "Settings",
"log": "Log",
"logout": "Logout"
},
"headline": {
"playlist": "Local or remote playlists",
"filter": "Filter playlist",
"xmltv": "Local or remote XMLTV files",
"mapping": "Map playlist channels to EPG channels",
"users": "User management",
"settings": "Settings",
"log": "Log",
"logout": "Logout"
}
},
"confirm":{
"restore": "All data will be replaced with those from the backup.Should the files be restored?"
},
"alert": {
"fileLoadingError": "File couldn't be loaded",
"invalidChannelNumber": "Invalid channel number"
},
"button":{
"back": "Back",
"backup": "Backup",
"bulkEdit": "Bulk Edit",
"cancel": "Cancel",
"delete": "Delete",
"done": "Done",
"login": "Login",
"new": "New",
"next": "Next",
"restore": "Restore",
"save": "Save",
"search": "Search",
"update": "Update",
"craeteAccount": "Create Account",
"resetlogs": "Reset Logs",
"uploadLogo": "Upload Logo"
},
"filter": {
"table": {
"name": "Filter Name",
"type": "Filter Type",
"filter": "Filter"
},
"custom": "Custom",
"group": "Group",
"name": {
"title": "Filter Name",
"placeholder": "Filter name",
"description": ""
},
"description": {
"title": "Description",
"placeholder": "Description",
"description": ""
},
"type": {
"title": "Type",
"groupTitle": "Group Title",
"customFilter": "Custom Filter"
},
"caseSensitive": {
"title": "Case Sensitive",
"placeholder": "",
"description": ""
},
"filterRule": {
"title": "Filter Rule",
"placeholder": "Sport {HD} !{ES,IT}",
"description": ""
},
"filterGroup": {
"title": "Group Title",
"placeholder": "",
"description": "Select a M3U group. (Counter)<br>Changing the group title in the M3U invalidates the filter."
},
"include": {
"title": "Include",
"placeholder": "FHD,UHD",
"description": "Channel name must include.<br>(Comma separated) Comma means or"
},
"exclude": {
"title": "Exclude",
"placeholder": "ES,IT",
"description": "Channel name must not contain.<br>(Comma separated) Comma means or"
}
},
"playlist": {
"table": {
"playlist": "Playlist",
"tuner": "Tuner",
"lastUpdate": "Last Update",
"availability": "Availability",
"type": "Type",
"streams": "Streams",
"groupTitle": "group-title",
"tvgID": "tvg-id",
"uniqueID": "Unique ID"
},
"playlistType": {
"title": "Playlist type",
"placeholder": "",
"description": ""
},
"type": {
"title": "Type",
"placeholder": "",
"description": ""
},
"name": {
"title": "Name",
"placeholder": "Playlist name",
"description": ""
},
"description": {
"title": "Description",
"placeholder": "Description",
"description": ""
},
"fileM3U": {
"title": "M3U File",
"placeholder": "File path or URL of the M3U",
"description": ""
},
"fileHDHR": {
"title": "HDHomeRun IP",
"placeholder": "IP address and port (192.168.1.10:5004)",
"description": ""
},
"tuner": {
"title": "Tuner / Streams",
"placeholder": "",
"description": "Number of parallel connections that can be established to the provider. <br>Only available with activated buffer.<br>New settings will only be applied after quitting all streams."
}
},
"xmltv": {
"table": {
"guide": "Guide",
"lastUpdate": "Last Update",
"availability": "Availability",
"channels": "Channels",
"programs": "Programs"
},
"name": {
"title": "Name",
"placeholder": "Guide name",
"description": ""
},
"description": {
"title": "Description",
"placeholder": "Description",
"description": ""
},
"fileXMLTV": {
"title": "XMLTV File",
"placeholder": "File path or URL of the XMLTV",
"description": ""
}
},
"mapping": {
"table": {
"chNo": "Ch. No.",
"logo": "Logo",
"channelName": "Channel Name",
"playlist": "Playlist",
"groupTitle": "Group Title",
"xmltvFile": "XMLTV File",
"xmltvID": "XMLTV ID"
},
"active": {
"title": "Active",
"placeholder": "",
"description": ""
},
"channelName": {
"title": "Channel Name",
"placeholder": "",
"description": ""
},
"updateChannelName": {
"title": "Update Channel Name",
"placeholder": "",
"description": ""
},
"channelLogo": {
"title": "Logo URL",
"placeholder": "",
"description": ""
},
"updateChannelLogo": {
"title": "Update Channel Logo",
"placeholder": "",
"description": ""
},
"epgCategory": {
"title": "EPG Category",
"placeholder": "",
"description": ""
},
"m3uGroupTitle": {
"title": "Group Title (xteve.m3u)",
"placeholder": "",
"description": ""
},
"xmltvFile": {
"title": "XMLTV File",
"placeholder": "",
"description": ""
},
"xmltvChannel": {
"title": "XMLTV Channel",
"placeholder": "",
"description": ""
}
},
"users": {
"table": {
"username": "Username",
"password": "Password",
"web": "WEB",
"pms": "PMS",
"m3u": "M3U",
"xml": "XML",
"api": "API"
},
"username": {
"title": "Username",
"placeholder": "Username",
"description": ""
},
"password": {
"title": "Password",
"placeholder": "Passoword",
"description": ""
},
"confirm": {
"title": "Confirm",
"placeholder": "Password confirm",
"description": ""
},
"web": {
"title": "Web Access",
"placeholder": "",
"description": ""
},
"pms": {
"title": "PMS Access",
"placeholder": "",
"description": ""
},
"m3u": {
"title": "M3U Access",
"placeholder": "",
"description": ""
},
"xml": {
"title": "XML Access",
"placeholder": "",
"description": ""
},
"api": {
"title": "API Access",
"placeholder": "",
"description": ""
}
},
"settings": {
"category": {
"general": "General",
"files": "Files",
"streaming": "Streaming",
"backup": "Backup",
"authentication": "Authentication"
},
"update": {
"title": "Schedule for updating (Playlist, XMLTV, Backup)",
"placeholder": "0000,1000,2000",
"description": "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated."
},
"api": {
"title": "API Interface",
"description": "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"
},
"epgSource": {
"title": "EPG Source",
"description": "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
},
"tuner":{
"title": "Number of Tuners",
"description": "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
},
"filesUpdate": {
"title": "Updates all files at startup",
"description": "Updates all playlists, tuner and XMLTV files at startup."
},
"cacheImages": {
"title": "Image caching",
"description": "All images from the XMLTV file are cached, allowing faster rendering of the grid in the client.<br>Downloading the images may take a while and will be done in the background."
},
"replaceEmptyImages": {
"title": "Replace missing program images",
"description": "If the poster in the XMLTV program is missing, the channel logo will be used."
},
"xteveAutoUpdate": {
"title": "Automatic update of xTeVe",
"description": "If a new version of xTeVe is available, it will be automatically installed. The updates are downloaded from GitHub."
},
"streamBuffering": {
"title": "Stream Buffer",
"description": "- The stream is passed from xTeVe to Plex / Emby / M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support"
},
"bufferSize": {
"title": "Buffer Size",
"description": "Buffer size in MB.<br>M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used."
},
"bufferTimeout": {
"title": "Timeout for new client connections",
"description": "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds.",
"placeholder": "100"
},
"userAgent": {
"title": "User agent",
"description": "User Agent for HTTP requests",
"placeholder": "xTeVe"
},
"backupPath": {
"title": "Location for automatic backups",
"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."
},
"tempPath": {
"title": "Location for the temporary files",
"placeholder": "/tmp/xteve/",
"description": "Location for the buffer files."
},
"backupKeep": {
"title": "Number of backups to keep",
"description": "Number of backups to keep. Older backups are automatically deleted."
},
"authenticationWEB": {
"title": "WEB Authentication",
"description": "Access to the web interface only possible with credentials."
},
"authenticationPMS": {
"title": "PMS Authentication",
"description": "Plex requests are only possible with authentication. <br><b>Warning!!!</b> After activating this function xTeVe must be delete in the PMS DVR settings and set up again."
},
"authenticationM3U": {
"title": "M3U Authentication",
"description": "Downloading the xteve.m3u file via an HTTP request is only possible with authentication."
},
"authenticationXML": {
"title": "XML Authentication",
"description": "Downloading the xteve.xml file via an HTTP request is only possible with authentication"
},
"authenticationAPI": {
"title": "API Authentication",
"description": "Access to the API interface is only possible with authentication."
}
},
"wizard": {
"epgSource": {
"title": "EPG Source",
"description": "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
},
"tuner":{
"title": "Number of tuners",
"description": "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
},
"m3u": {
"title": "M3U Playlist",
"description": "Local or remote playlists"
},
"xmltv": {
"title": "XMLTV File",
"description": "Local or remote XMLTV file"
}
},
"login": {
"failed": "User authentication failed",
"headline": "Login",
"username": {
"title": "Username",
"placeholder": "Username"
},
"password": {
"title": "Password",
"placeholder": "Password"
}
},
"account": {
"failed": "Password does not match",
"headline": "Create user account",
"username": {
"title": "Username",
"placeholder": "Username"
},
"password": {
"title": "Password",
"placeholder": "Password"
},
"confirm": {
"title": "Confirm",
"placeholder": "Confirm"
}
}
}

46
html/login.html Normal file
View File

@@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css">
<script language="javascript" type="text/javascript" src="js/network_ts.js"></script>
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head>
<body>
<div id="header" class="imgCenter"></div>
<div id="box">
<div id="headline">
<h1 id="head-text" class="center">{{.login.headline}}</h1>
</div>
<p id="err" class="errorMsg center">{{.authenticationErr}}</p>
<div id="content">
<form id="authentication" action="/web/" method="post">
<h5>{{.login.username.title}}:</h5>
<input id="username" type="text" name="username" placeholder="Username" value="">
<h5>{{.login.password.title}}:</h5>
<input id="password" type="password" name="password" placeholder="Password" value="">
</form>
</div>
<div id="box-footer">
<input id="submit" class="" type="button" value="{{.button.login}}" onclick="javascript: login();">
</div>
</div>
</body>
</html>

30
html/maintenance.html Normal file
View File

@@ -0,0 +1,30 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css">
</head>
<body>
<div id="header" class="imgCenter"></div>
<div id="box">
<div id="headline">
<h1 id="head-text" class="center">Maintenance</h1>
</div>
<div id="content">
xTeVe is updating the database, please try again later.
</div>
<div id="box-footer"></div>
</div>
</body>
</html>

BIN
html/video/stream-limit.ts Normal file

Binary file not shown.

169
src/authentication.go Normal file
View File

@@ -0,0 +1,169 @@
package src
import (
"encoding/base64"
"errors"
"net/http"
"strings"
"../src/internal/authentication"
)
func activatedSystemAuthentication() (err error) {
err = authentication.Init(System.Folder.Config, 60)
if err != nil {
return
}
var defaults = make(map[string]interface{})
defaults["authentication.web"] = false
defaults["authentication.pms"] = false
defaults["authentication.xml"] = false
defaults["authentication.api"] = false
err = authentication.SetDefaultUserData(defaults)
return
}
func createFirstUserForAuthentication(username, password string) (token string, err error) {
var authenticationErr = func(err error) {
if err != nil {
return
}
}
err = authentication.CreateDefaultUser(username, password)
authenticationErr(err)
token, err = authentication.UserAuthentication(username, password)
authenticationErr(err)
token, err = authentication.CheckTheValidityOfTheToken(token)
authenticationErr(err)
var userData = make(map[string]interface{})
userData["username"] = username
userData["authentication.web"] = true
userData["authentication.pms"] = true
userData["authentication.m3u"] = true
userData["authentication.xml"] = true
userData["authentication.api"] = false
userData["defaultUser"] = true
userID, err := authentication.GetUserID(token)
authenticationErr(err)
err = authentication.WriteUserData(userID, userData)
authenticationErr(err)
return
}
func tokenAuthentication(token string) (newToken string, err error) {
if System.ConfigurationWizard == true {
return
}
newToken, err = authentication.CheckTheValidityOfTheToken(token)
return
}
func basicAuth(r *http.Request, level string) (username string, err error) {
err = errors.New("User authentication failed")
auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(auth) != 2 || auth[0] != "Basic" {
return
}
payload, _ := base64.StdEncoding.DecodeString(auth[1])
pair := strings.SplitN(string(payload), ":", 2)
username = pair[0]
var password = pair[1]
token, err := authentication.UserAuthentication(username, password)
if err != nil {
return
}
err = checkAuthorizationLevel(token, level)
return
}
func urlAuth(r *http.Request, requestType string) (err error) {
var level, token string
var username = r.URL.Query().Get("username")
var password = r.URL.Query().Get("password")
switch requestType {
case "m3u":
level = "authentication.m3u"
if Settings.AuthenticationM3U == true {
token, err = authentication.UserAuthentication(username, password)
if err != nil {
return
}
err = checkAuthorizationLevel(token, level)
}
case "xml":
level = "authentication.xml"
if Settings.AuthenticationXML == true {
token, err = authentication.UserAuthentication(username, password)
if err != nil {
return
}
err = checkAuthorizationLevel(token, level)
}
}
return
}
func checkAuthorizationLevel(token, level string) (err error) {
var authenticationErr = func(err error) {
if err != nil {
return
}
}
userID, err := authentication.GetUserID(token)
authenticationErr(err)
userData, err := authentication.ReadUserData(userID)
authenticationErr(err)
if len(userData) > 0 {
if v, ok := userData[level].(bool); ok {
if v == false {
err = errors.New("No authorization")
}
} else {
userData[level] = false
err = authentication.WriteUserData(userID, userData)
err = errors.New("No authorization")
}
} else {
err = authentication.WriteUserData(userID, userData)
err = errors.New("No authorization")
}
return
}

191
src/backup.go Normal file
View File

@@ -0,0 +1,191 @@
package src
import (
b64 "encoding/base64"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func xTeVeAutoBackup() (err error) {
var archiv = "xteve_auto_backup_" + time.Now().Format("20060102_1504") + ".zip"
var target string
var sourceFiles = make([]string, 0)
var oldBackupFiles = make([]string, 0)
var debug string
if len(Settings.BackupPath) > 0 {
System.Folder.Backup = Settings.BackupPath
}
showInfo("Backup Path:" + System.Folder.Backup)
err = checkFolder(System.Folder.Backup)
if err != nil {
ShowError(err, 1070)
return
}
// Alte Backups löschen
files, err := ioutil.ReadDir(System.Folder.Backup)
if err == nil {
for _, file := range files {
if filepath.Ext(file.Name()) == ".zip" && strings.Contains(file.Name(), "xteve_auto_backup") {
oldBackupFiles = append(oldBackupFiles, file.Name())
}
}
// Alle Backups löschen
var end int
switch Settings.BackupKeep {
case 0:
end = 0
default:
end = Settings.BackupKeep - 1
}
for i := 0; i < len(oldBackupFiles)-end; i++ {
os.RemoveAll(System.Folder.Backup + oldBackupFiles[i])
debug = fmt.Sprintf("Delete backup file:%s", oldBackupFiles[i])
showDebug(debug, 1)
}
if Settings.BackupKeep == 0 {
return
}
} else {
return
}
// Backup erstellen
if err == nil {
target = System.Folder.Backup + archiv
for _, i := range SystemFiles {
sourceFiles = append(sourceFiles, System.Folder.Config+i)
}
sourceFiles = append(sourceFiles, System.Folder.ImagesUpload)
err = zipFiles(sourceFiles, target)
if err == nil {
debug = fmt.Sprintf("Create backup file:%s", target)
showDebug(debug, 1)
showInfo("Backup file:" + target)
}
}
return
}
func xteveBackup() (archiv string, err error) {
err = checkFolder(System.Folder.Temp)
if err != nil {
return
}
archiv = "xteve_backup_" + time.Now().Format("20060102_1504") + ".zip"
var target = System.Folder.Temp + archiv
var sourceFiles = make([]string, 0)
for _, i := range SystemFiles {
sourceFiles = append(sourceFiles, System.Folder.Config+i)
}
sourceFiles = append(sourceFiles, System.Folder.Data)
err = zipFiles(sourceFiles, target)
if err != nil {
ShowError(err, 0)
return
}
return
}
func xteveRestore(input string) (newWebURL string, err error) {
var newPort, oldPort string
// Base64 Json String in base64 umwandeln
b64data := input[strings.IndexByte(input, ',')+1:]
// Base64 in bytes umwandeln und speichern
sDec, err := b64.StdEncoding.DecodeString(b64data)
if err != nil {
return
}
var archive = System.Folder.Temp + "restore.zip"
err = writeByteToFile(archive, sDec)
if err != nil {
return
}
// Zip Archiv entpacken
err = extractZIP(archive, System.Folder.Config)
if err != nil {
return
}
// Neue Config laden um den Port zu überprüfen
newConfig, err := loadJSONFileToMap(System.Folder.Config + "settings.json")
if err != nil {
ShowError(err, 0)
return
}
newPort = newConfig["port"].(string)
oldPort = Settings.Port
if newPort == oldPort {
if err != nil {
ShowError(err, 0)
}
loadSettings()
err := Init()
if err != nil {
ShowError(err, 0)
return "", err
}
err = StartSystem(true)
if err != nil {
ShowError(err, 0)
return "", err
}
return "", err
}
var url = System.URLBase + "/web/"
newWebURL = strings.Replace(url, ":"+oldPort, ":"+newPort, 1)
return
}

1405
src/buffer.go Normal file

File diff suppressed because it is too large Load Diff

148
src/compression.go Normal file
View File

@@ -0,0 +1,148 @@
package src
import (
"archive/zip"
"bytes"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
)
func zipFiles(sourceFiles []string, target string) error {
zipfile, err := os.Create(target)
if err != nil {
return err
}
defer zipfile.Close()
archive := zip.NewWriter(zipfile)
defer archive.Close()
for _, source := range sourceFiles {
info, err := os.Stat(source)
if err != nil {
return nil
}
var baseDir string
if info.IsDir() {
baseDir = filepath.Base(System.Folder.Data)
}
filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
if baseDir != "" {
header.Name = filepath.Join(strings.TrimPrefix(path, System.Folder.Config))
}
if info.IsDir() {
header.Name += string(os.PathSeparator)
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
return err
})
}
return err
}
func extractZIP(archive, target string) (err error) {
reader, err := zip.OpenReader(archive)
if err != nil {
return err
}
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
for _, file := range reader.File {
path := filepath.Join(target, file.Name)
if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
defer targetFile.Close()
if _, err := io.Copy(targetFile, fileReader); err != nil {
return err
}
}
return
}
func extractGZIP(gzipBody []byte, fileSource string) (body []byte, err error) {
var b = bytes.NewBuffer(gzipBody)
var r io.Reader
r, err = gzip.NewReader(b)
if err != nil {
// Keine gzip Datei
body = gzipBody
err = nil
return
}
showInfo("Extract gzip:" + fileSource)
var resB bytes.Buffer
_, err = resB.ReadFrom(r)
if err != nil {
body = gzipBody
err = nil
return
}
body = resB.Bytes()
return
}

242
src/config.go Normal file
View File

@@ -0,0 +1,242 @@
package src
import (
"fmt"
"os"
"runtime"
"strings"
"sync"
)
// System : Beinhaltet alle Systeminformationen
var System SystemStruct
// WebScreenLog : Logs werden im RAM gespeichert und für das Webinterface bereitgestellt
var WebScreenLog WebScreenLogStruct
// Settings : Inhalt der settings.json
var Settings SettingsStrcut
// Data : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
var Data DataStruct
// SystemFiles : Alle Systemdateien
var SystemFiles = []string{"authentication.json", "pms.json", "settings.json", "xepg.json", "urls.json"}
// BufferInformation : Informationen über den Buffer (aktive Streams, maximale Streams)
var BufferInformation sync.Map
// BufferClients : Anzahl der Clients die einen Stream über den Buffer abspielen
var BufferClients sync.Map
// Init : Systeminitialisierung
func Init() (err error) {
var debug string
// System Einstellungen
System.AppName = strings.ToLower(System.Name)
System.ARCH = runtime.GOARCH
System.OS = runtime.GOOS
System.ServerProtocol.API = "http"
System.ServerProtocol.DVR = "http"
System.ServerProtocol.M3U = "http"
System.ServerProtocol.WEB = "http"
System.ServerProtocol.XML = "http"
System.DVRLimit = 480
System.Compatibility = "1.4.4"
// Default Logeinträge, wird später von denen aus der settings.json überschrieben. Muss gemacht werden, damit die ersten Einträge auch im Log (webUI aangezeigt werden)
Settings.LogEntriesRAM = 500
// Variablen für den Update Prozess
//System.Update.Git = "https://github.com/xteve-project/xTeVe-Downloads/blob"
System.Update.Git = fmt.Sprintf("https://github.com/%s/%s/blob", System.GitHub.User, System.GitHub.Repo)
System.Update.Name = "xteve_2"
// Ordnerpfade festlegen
var tempFolder = os.TempDir() + string(os.PathSeparator) + System.AppName + string(os.PathSeparator)
tempFolder = getPlatformPath(strings.Replace(tempFolder, "//", "/", -1))
if len(System.Folder.Config) == 0 {
System.Folder.Config = GetUserHomeDirectory() + string(os.PathSeparator) + "." + System.AppName + string(os.PathSeparator)
} else {
System.Folder.Config = strings.TrimRight(System.Folder.Config, string(os.PathSeparator)) + string(os.PathSeparator)
}
System.Folder.Config = getPlatformPath(System.Folder.Config)
System.Folder.Backup = System.Folder.Config + "backup" + string(os.PathSeparator)
System.Folder.Data = System.Folder.Config + "data" + string(os.PathSeparator)
System.Folder.Cache = System.Folder.Config + "cache" + string(os.PathSeparator)
System.Folder.ImagesCache = System.Folder.Cache + "images" + string(os.PathSeparator)
System.Folder.ImagesUpload = System.Folder.Data + "images" + string(os.PathSeparator)
System.Folder.Temp = tempFolder
// Dev Info
showDevInfo()
// System Ordner erstellen
err = createSystemFolders()
if err != nil {
ShowError(err, 1070)
return
}
System.File.XML = getPlatformFile(fmt.Sprintf("%s%s.xml", System.Folder.Data, System.AppName))
System.File.M3U = getPlatformFile(fmt.Sprintf("%s%s.m3u", System.Folder.Data, System.AppName))
err = activatedSystemAuthentication()
if err != nil {
return
}
err = resolveHostIP()
if err != nil {
ShowError(err, 1002)
}
// Menü für das Webinterface
System.WEB.Menu = []string{"playlist", "filter", "xmltv", "mapping", "users", "settings", "log", "logout"}
fmt.Println("For help run: " + getPlatformFile(os.Args[0]) + " -h")
fmt.Println()
// Überprüfen ob xTeVe als root läuft
if os.Geteuid() == 0 {
showWarning(2010)
}
if System.Flag.Debug > 0 {
debug = fmt.Sprintf("Debug Level:%d", System.Flag.Debug)
showDebug(debug, 1)
}
showInfo(fmt.Sprintf("Version:%s Build: %s", System.Version, System.Build))
showInfo(fmt.Sprintf("System IP Addresses:IPv4: %d | IPv6: %d", len(System.IPAddressesV4), len(System.IPAddressesV6)))
showInfo("Hostname:" + System.Hostname)
showInfo(fmt.Sprintf("System Folder:%s", getPlatformPath(System.Folder.Config)))
// Systemdateien erstellen (Falls nicht vorhanden)
err = createSystemFiles()
if err != nil {
ShowError(err, 1071)
return
}
// Bedingte Update Änderungen durchführen
err = conditionalUpdateChanges()
if err != nil {
ShowError(err, 0)
return
}
// Einstellungen laden (settings.json)
showInfo(fmt.Sprintf("Load Settings:%s", System.File.Settings))
_, err = loadSettings()
if err != nil {
ShowError(err, 0)
return
}
// Berechtigung aller Ordner überprüfen
err = checkFilePermission(System.Folder.Config)
if err == nil {
err = checkFilePermission(System.Folder.Temp)
}
// Separaten tmp Ordner für jede Instanz
//System.Folder.Temp = System.Folder.Temp + Settings.UUID + string(os.PathSeparator)
showInfo(fmt.Sprintf("Temporary Folder:%s", getPlatformPath(System.Folder.Temp)))
err = checkFolder(System.Folder.Temp)
if err != nil {
return
}
err = removeChildItems(getPlatformPath(System.Folder.Temp))
if err != nil {
return
}
// DLNA Server starten
go SSDP()
// Branch festlegen
System.Branch = Settings.Branch
if System.Dev == true {
System.Branch = "Development"
}
if len(System.Branch) == 0 {
System.Branch = "master"
}
showInfo(fmt.Sprintf("GitHub:https://github.com/%s", System.GitHub.User))
showInfo(fmt.Sprintf("Git Branch:%s [%s]", System.Branch, System.GitHub.User))
// Domainnamen setzten
setGlobalDomain(fmt.Sprintf("%s:%s", System.IPAddress, Settings.Port))
System.URLBase = fmt.Sprintf("%s://%s:%s", System.ServerProtocol.WEB, System.IPAddress, Settings.Port)
// HTML Dateien erstellen, mit dev == true werden die lokalen HTML Dateien verwendet
if System.Dev == true {
HTMLInit("webUI", "src", "html"+string(os.PathSeparator), "src"+string(os.PathSeparator)+"webUI.go")
err = BuildGoFile()
if err != nil {
return
}
}
loadHTMLMap()
return
}
// StartSystem : System wird gestartet
func StartSystem(updateProviderFiles bool) (err error) {
setDeviceID()
if System.ScanInProgress == 1 {
return
}
// Systeminformationen in der Konsole ausgeben
showInfo(fmt.Sprintf("UUID:%s", Settings.UUID))
showInfo(fmt.Sprintf("Tuner (Plex / Emby):%d", Settings.Tuner))
showInfo(fmt.Sprintf("EPG Source:%s", Settings.EpgSource))
showInfo(fmt.Sprintf("Plex Channel Limit:%d", System.DVRLimit))
// Providerdaten aktualisieren
if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate == true || updateProviderFiles == true {
err = xTeVeAutoBackup()
if err != nil {
ShowError(err, 1090)
}
getProviderData("m3u", "")
getProviderData("hdhr", "")
if Settings.EpgSource == "XEPG" {
getProviderData("xmltv", "")
}
}
err = buildDatabaseDVR()
if err != nil {
ShowError(err, 0)
return
}
buildXEPG(false)
return
}

953
src/data.go Normal file
View File

@@ -0,0 +1,953 @@
package src
import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
"../src/internal/authentication"
)
// Einstellungen ändern (WebUI)
func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err error) {
var oldSettings = jsonToMap(mapToJSON(Settings))
var newSettings = jsonToMap(mapToJSON(request.Settings))
var reloadData = false
var cacheImages = false
var createXEPGFiles = false
var debug string
for key, value := range newSettings {
if _, ok := oldSettings[key]; ok {
switch key {
case "tuner":
showWarning(2105)
case "epgSource":
reloadData = true
case "update":
// Die Formatierung der Uhrzeit überprüfen (0000 - 2359)
for _, i := range newSettings[key].([]interface{}) {
_, err := time.Parse("1504", i.(string))
if err != nil {
ShowError(err, 1012)
return Settings, err
}
}
case "cache.images":
cacheImages = true
case "xepg.replace.missing.images":
createXEPGFiles = true
case "backup.path":
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
err = checkFolder(value.(string))
if err == nil {
err = checkFilePermission(value.(string))
if err != nil {
return
}
}
if err != nil {
return
}
case "temp.path":
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
err = checkFolder(value.(string))
if err == nil {
err = checkFilePermission(value.(string))
if err != nil {
return
}
}
if err != nil {
return
}
}
oldSettings[key] = value
switch fmt.Sprintf("%T", value) {
case "bool":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
case "string":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
case "[]interface {}":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
case "float64":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
default:
debug = fmt.Sprintf("%T", value)
}
showDebug(debug, 1)
}
}
// Einstellungen aktualisieren
err = json.Unmarshal([]byte(mapToJSON(oldSettings)), &Settings)
if err != nil {
return
}
if Settings.AuthenticationWEB == false {
Settings.AuthenticationAPI = false
Settings.AuthenticationM3U = false
Settings.AuthenticationPMS = false
Settings.AuthenticationWEB = false
Settings.AuthenticationXML = false
}
err = saveSettings(Settings)
if err == nil {
settings = Settings
if reloadData == true {
err = buildDatabaseDVR()
if err != nil {
return
}
buildXEPG(false)
}
if cacheImages == true {
if Settings.EpgSource == "XEPG" {
go func() {
if Settings.CacheImages == true {
createXMLTVFile()
cachingImages()
createXMLTVFile()
createM3UFile()
} else {
createXMLTVFile()
createM3UFile()
}
}()
}
}
if createXEPGFiles == true {
go func() {
createXMLTVFile()
createM3UFile()
}()
}
}
return
}
// Providerdaten speichern (WebUI)
func saveFiles(request RequestStruct, fileType string) (err error) {
var filesMap = make(map[string]interface{})
var newData = make(map[string]interface{})
var indicator string
var reloadData = false
switch fileType {
case "m3u":
filesMap = Settings.Files.M3U
newData = request.Files.M3U
indicator = "M"
case "hdhr":
filesMap = Settings.Files.HDHR
newData = request.Files.HDHR
indicator = "H"
case "xmltv":
filesMap = Settings.Files.XMLTV
newData = request.Files.XMLTV
indicator = "X"
}
if len(filesMap) == 0 {
filesMap = make(map[string]interface{})
}
for dataID, data := range newData {
if dataID == "-" {
// Neue Providerdatei
dataID = indicator + randomString(19)
data.(map[string]interface{})["new"] = true
filesMap[dataID] = data
} else {
// Bereits vorhandene Providerdatei
for key, value := range data.(map[string]interface{}) {
var oldData = filesMap[dataID].(map[string]interface{})
oldData[key] = value
}
}
switch fileType {
case "m3u":
Settings.Files.M3U = filesMap
case "hdhr":
Settings.Files.HDHR = filesMap
case "xmltv":
Settings.Files.XMLTV = filesMap
}
// Neue Providerdatei
if _, ok := data.(map[string]interface{})["new"]; ok {
reloadData = true
err = getProviderData(fileType, dataID)
delete(data.(map[string]interface{}), "new")
if err != nil {
delete(filesMap, dataID)
return
}
}
if _, ok := data.(map[string]interface{})["delete"]; ok {
deleteLocalProviderFiles(dataID, fileType)
reloadData = true
}
err = saveSettings(Settings)
if err != nil {
return
}
if reloadData == true {
err = buildDatabaseDVR()
if err != nil {
return err
}
buildXEPG(false)
}
Settings, _ = loadSettings()
}
return
}
// Providerdaten manuell aktualisieren (WebUI)
func updateFile(request RequestStruct, fileType string) (err error) {
var updateData = make(map[string]interface{})
switch fileType {
case "m3u":
updateData = request.Files.M3U
case "hdhr":
updateData = request.Files.HDHR
case "xmltv":
updateData = request.Files.XMLTV
}
for dataID := range updateData {
err = getProviderData(fileType, dataID)
if err == nil {
err = buildDatabaseDVR()
buildXEPG(false)
}
}
return
}
// Providerdaten löschen (WebUI)
func deleteLocalProviderFiles(dataID, fileType string) {
var removeData = make(map[string]interface{})
var fileExtension string
switch fileType {
case "m3u":
removeData = Settings.Files.M3U
fileExtension = ".m3u"
case "hdhr":
removeData = Settings.Files.HDHR
fileExtension = ".json"
case "xmltv":
removeData = Settings.Files.XMLTV
fileExtension = ".xml"
}
if _, ok := removeData[dataID]; ok {
delete(removeData, dataID)
os.RemoveAll(System.Folder.Data + dataID + fileExtension)
}
return
}
// Filtereinstellungen speichern (WebUI)
func saveFilter(request RequestStruct) (settings SettingsStrcut, err error) {
var filterMap = make(map[int64]interface{})
var newData = make(map[int64]interface{})
var defaultFilter FilterStruct
defaultFilter.Active = true
defaultFilter.CaseSensitive = false
filterMap = Settings.Filter
newData = request.Filter
var createNewID = func() (id int64) {
newID:
if _, ok := filterMap[id]; ok {
id++
goto newID
}
return id
}
for dataID, data := range newData {
if dataID == -1 {
// Neuer Filter
dataID = createNewID()
filterMap[dataID] = jsonToMap(mapToJSON(defaultFilter))
}
// Filter aktualisieren / löschen
for key, value := range data.(map[string]interface{}) {
var oldData = filterMap[dataID].(map[string]interface{})
oldData[key] = value
// Filter löschen
if _, ok := data.(map[string]interface{})["delete"]; ok {
delete(filterMap, dataID)
break
}
}
}
err = saveSettings(Settings)
if err != nil {
return
}
settings = Settings
err = buildDatabaseDVR()
if err != nil {
return
}
buildXEPG(false)
return
}
// XEPG Mapping speichern
func saveXEpgMapping(request RequestStruct) (err error) {
var tmp = Data.XEPG
err = json.Unmarshal([]byte(mapToJSON(request.EpgMapping)), &tmp)
if err != nil {
return
}
err = saveMapToJSONFile(System.File.XEPG, request.EpgMapping)
if err != nil {
return err
}
Data.XEPG.Channels = request.EpgMapping
cleanupXEPG()
buildXEPG(true)
return
}
// Benutzerdaten speichern (WebUI)
func saveUserData(request RequestStruct) (err error) {
var userData = request.UserData
var newCredentials = func(userID string, newUserData map[string]interface{}) (err error) {
var newUsername, newPassword string
if username, ok := newUserData["username"].(string); ok {
newUsername = username
}
if password, ok := newUserData["password"].(string); ok {
newPassword = password
}
if len(newUsername) > 0 {
err = authentication.ChangeCredentials(userID, newUsername, newPassword)
}
return
}
for userID, newUserData := range userData {
err = newCredentials(userID, newUserData.(map[string]interface{}))
if err != nil {
return
}
if request.DeleteUser == true {
err = authentication.RemoveUser(userID)
return
}
delete(newUserData.(map[string]interface{}), "password")
delete(newUserData.(map[string]interface{}), "confirm")
if _, ok := newUserData.(map[string]interface{})["delete"]; ok {
authentication.RemoveUser(userID)
} else {
err = authentication.WriteUserData(userID, newUserData.(map[string]interface{}))
if err != nil {
return
}
}
}
return
}
// Neuen Benutzer anlegen (WebUI)
func saveNewUser(request RequestStruct) (err error) {
var data = request.UserData
var username = data["username"].(string)
var password = data["password"].(string)
delete(data, "password")
delete(data, "confirm")
userID, err := authentication.CreateNewUser(username, password)
if err != nil {
return
}
err = authentication.WriteUserData(userID, data)
return
}
// Wizard (WebUI)
func saveWizard(request RequestStruct) (nextStep int, err error) {
var wizard = jsonToMap(mapToJSON(request.Wizard))
for key, value := range wizard {
switch key {
case "tuner":
Settings.Tuner = int(value.(float64))
nextStep = 1
case "epgSource":
Settings.EpgSource = value.(string)
nextStep = 2
case "m3u", "xmltv":
var filesMap = make(map[string]interface{})
var data = make(map[string]interface{})
var indicator, dataID string
filesMap = make(map[string]interface{})
data["type"] = key
data["new"] = true
switch key {
case "m3u":
filesMap = Settings.Files.M3U
data["name"] = "M3U"
indicator = "M"
case "xmltv":
filesMap = Settings.Files.XMLTV
data["name"] = "XMLTV"
indicator = "X"
}
dataID = indicator + randomString(19)
data["file.source"] = value.(string)
filesMap[dataID] = data
switch key {
case "m3u":
Settings.Files.M3U = filesMap
nextStep = 3
err = getProviderData(key, dataID)
if err != nil {
ShowError(err, 000)
delete(filesMap, dataID)
return
}
err = buildDatabaseDVR()
if err != nil {
ShowError(err, 000)
delete(filesMap, dataID)
return
}
if Settings.EpgSource == "PMS" {
nextStep = 10
}
case "xmltv":
Settings.Files.XMLTV = filesMap
nextStep = 10
err = getProviderData(key, dataID)
if err != nil {
ShowError(err, 000)
delete(filesMap, dataID)
return
}
buildXEPG(false)
}
}
}
err = saveSettings(Settings)
if err != nil {
return
}
return
}
// Filterregeln erstellen
func createFilterRules() (err error) {
Data.Filter = nil
var dataFilter Filter
for _, f := range Settings.Filter {
var filter FilterStruct
var exclude, include string
err = json.Unmarshal([]byte(mapToJSON(f)), &filter)
if err != nil {
return
}
switch filter.Type {
case "custom-filter":
dataFilter.CaseSensitive = filter.CaseSensitive
dataFilter.Rule = filter.Filter
dataFilter.Type = filter.Type
Data.Filter = append(Data.Filter, dataFilter)
case "group-title":
if len(filter.Include) > 0 {
include = fmt.Sprintf(" {%s}", filter.Include)
}
if len(filter.Exclude) > 0 {
exclude = fmt.Sprintf(" !{%s}", filter.Exclude)
}
dataFilter.CaseSensitive = filter.CaseSensitive
dataFilter.Rule = fmt.Sprintf("%s%s%s", filter.Filter, include, exclude)
dataFilter.Type = filter.Type
Data.Filter = append(Data.Filter, dataFilter)
}
}
return
}
// Datenbank für das DVR System erstellen
func buildDatabaseDVR() (err error) {
System.ScanInProgress = 1
Data.Streams.All = make([]interface{}, 0)
Data.Streams.Active = make([]interface{}, 0)
Data.Streams.Inactive = make([]interface{}, 0)
Data.Playlist.M3U.Groups.Text = []string{}
Data.Playlist.M3U.Groups.Value = []string{}
Data.StreamPreviewUI.Active = []string{}
Data.StreamPreviewUI.Inactive = []string{}
var availableFileTypes = []string{"m3u", "hdhr"}
var tmpGroupsM3U = make(map[string]int64)
err = createFilterRules()
if err != nil {
return
}
for _, fileType := range availableFileTypes {
var playlistFile = getLocalProviderFiles(fileType)
for n, i := range playlistFile {
var channels []interface{}
var groupTitle, tvgID, uuid int = 0, 0, 0
var keys = []string{"group-title", "tvg-id", "uuid"}
var compatibility = make(map[string]int)
var id = strings.TrimSuffix(getFilenameFromPath(i), path.Ext(getFilenameFromPath(i)))
var playlistName = getProviderParameter(id, fileType, "name")
switch fileType {
case "m3u":
channels, err = parsePlaylist(i, fileType)
case "hdhr":
channels, err = parsePlaylist(i, fileType)
}
if err != nil {
ShowError(err, 1005)
err = errors.New(playlistName + ": Local copy of the file no longer exists")
ShowError(err, 0)
playlistFile = append(playlistFile[:n], playlistFile[n+1:]...)
}
// Streams analysieren
for _, stream := range channels {
var s = stream.(map[string]string)
s["_file.m3u.path"] = i
s["_file.m3u.name"] = playlistName
s["_file.m3u.id"] = id
// Kompatibilität berechnen
for _, key := range keys {
switch key {
case "uuid":
if value, ok := s["_uuid.key"]; ok {
if len(value) > 0 {
uuid++
}
}
case "group-title":
if value, ok := s[key]; ok {
if len(value) > 0 {
if _, ok := tmpGroupsM3U[value]; ok {
tmpGroupsM3U[value]++
} else {
tmpGroupsM3U[value] = 1
}
groupTitle++
}
}
case "tvg-id":
if value, ok := s[key]; ok {
if len(value) > 0 {
tvgID++
}
}
}
}
Data.Streams.All = append(Data.Streams.All, stream)
// Neuer Filter ab Version 1.3.0
var preview string
var status = filterThisStream(stream)
if name, ok := s["name"]; ok {
var group string
if v, ok := s["group-title"]; ok {
group = v
}
preview = fmt.Sprintf("%s [%s]", name, group)
}
switch status {
case true:
Data.StreamPreviewUI.Active = append(Data.StreamPreviewUI.Active, preview)
Data.Streams.Active = append(Data.Streams.Active, stream)
case false:
Data.StreamPreviewUI.Inactive = append(Data.StreamPreviewUI.Inactive, preview)
Data.Streams.Inactive = append(Data.Streams.Inactive, stream)
}
}
if tvgID == 0 {
compatibility["tvg.id"] = 0
} else {
compatibility["tvg.id"] = int(tvgID * 100 / len(channels))
}
if groupTitle == 0 {
compatibility["group.title"] = 0
} else {
compatibility["group.title"] = int(groupTitle * 100 / len(channels))
}
if uuid == 0 {
compatibility["stream.id"] = 0
} else {
compatibility["stream.id"] = int(uuid * 100 / len(channels))
}
compatibility["streams"] = len(channels)
setProviderCompatibility(id, fileType, compatibility)
}
}
for group, count := range tmpGroupsM3U {
var text = fmt.Sprintf("%s (%d)", group, count)
var value = fmt.Sprintf("%s", group)
Data.Playlist.M3U.Groups.Text = append(Data.Playlist.M3U.Groups.Text, text)
Data.Playlist.M3U.Groups.Value = append(Data.Playlist.M3U.Groups.Value, value)
}
sort.Strings(Data.Playlist.M3U.Groups.Text)
sort.Strings(Data.Playlist.M3U.Groups.Value)
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.DVRLimit && len(Settings.Filter) == 0 {
Data.Streams.Active = Data.Streams.All
Data.Streams.Inactive = make([]interface{}, 0)
Data.StreamPreviewUI.Active = Data.StreamPreviewUI.Inactive
Data.StreamPreviewUI.Inactive = []string{}
}
if len(Data.Streams.Active) > System.DVRLimit {
showWarning(2000)
}
if len(Settings.Filter) == 0 && len(Data.Streams.All) > System.DVRLimit {
showWarning(2001)
}
System.ScanInProgress = 0
showInfo(fmt.Sprintf("All streams:%d", len(Data.Streams.All)))
showInfo(fmt.Sprintf("Active streams:%d", len(Data.Streams.Active)))
showInfo(fmt.Sprintf("Filter:%d", len(Data.Filter)))
sort.Strings(Data.StreamPreviewUI.Active)
sort.Strings(Data.StreamPreviewUI.Inactive)
return
}
// Speicherort aller lokalen Providerdateien laden, immer für eine Dateityp (M3U, XMLTV usw.)
func getLocalProviderFiles(fileType string) (localFiles []string) {
var fileExtension string
var dataMap = make(map[string]interface{})
switch fileType {
case "m3u":
fileExtension = ".m3u"
dataMap = Settings.Files.M3U
case "hdhr":
fileExtension = ".json"
dataMap = Settings.Files.HDHR
case "xmltv":
fileExtension = ".xml"
dataMap = Settings.Files.XMLTV
}
for dataID := range dataMap {
localFiles = append(localFiles, System.Folder.Data+dataID+fileExtension)
}
return
}
// Providerparameter anhand von dem Key ausgeben
func getProviderParameter(id, fileType, key string) (s string) {
var dataMap = make(map[string]interface{})
switch fileType {
case "m3u":
dataMap = Settings.Files.M3U
case "hdhr":
dataMap = Settings.Files.HDHR
case "xmltv":
dataMap = Settings.Files.XMLTV
}
if data, ok := dataMap[id].(map[string]interface{}); ok {
if v, ok := data[key].(string); ok {
s = v
}
if v, ok := data[key].(float64); ok {
s = strconv.Itoa(int(v))
}
}
return
}
// Provider Statistiken Kompatibilität aktualisieren
func setProviderCompatibility(id, fileType string, compatibility map[string]int) {
var dataMap = make(map[string]interface{})
switch fileType {
case "m3u":
dataMap = Settings.Files.M3U
case "hdhr":
dataMap = Settings.Files.HDHR
case "xmltv":
dataMap = Settings.Files.XMLTV
}
if data, ok := dataMap[id].(map[string]interface{}); ok {
data["compatibility"] = compatibility
switch fileType {
case "m3u":
Settings.Files.M3U = dataMap
case "hdhr":
Settings.Files.HDHR = dataMap
case "xmltv":
Settings.Files.XMLTV = dataMap
}
saveSettings(Settings)
}
}

236
src/hdhr.go Normal file
View File

@@ -0,0 +1,236 @@
package src
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
)
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []interface{}, err error) {
var hdhrData []interface{}
err = json.Unmarshal(content, &hdhrData)
if err == nil {
for _, d := range hdhrData {
var channel = make(map[string]string)
var data = d.(map[string]interface{})
channel["group-title"] = playlistName
channel["name"] = data["GuideName"].(string)
channel["tvg-id"] = data["GuideName"].(string)
channel["url"] = data["URL"].(string)
channel["ID-"+id] = data["GuideNumber"].(string)
channel["_uuid.key"] = "ID-" + id
channel["_values"] = playlistName + " " + channel["name"]
channels = append(channels, channel)
}
}
return
}
func getCapability() (xmlContent []byte, err error) {
var capability Capability
var buffer bytes.Buffer
capability.Xmlns = "urn:schemas-upnp-org:device-1-0"
capability.URLBase = System.ServerProtocol.WEB + "://" + System.Domain
capability.SpecVersion.Major = 1
capability.SpecVersion.Minor = 0
capability.Device.DeviceType = "urn:schemas-upnp-org:device:MediaServer:1"
capability.Device.FriendlyName = System.Name
capability.Device.Manufacturer = "Silicondust"
capability.Device.ModelName = "HDTC-2US"
capability.Device.ModelNumber = "HDTC-2US"
capability.Device.SerialNumber = ""
capability.Device.UDN = "uuid:" + System.DeviceID
output, err := xml.MarshalIndent(capability, " ", " ")
if err != nil {
ShowError(err, 1003)
}
buffer.Write([]byte(xml.Header))
buffer.Write([]byte(output))
xmlContent = buffer.Bytes()
return
}
func getDiscover() (jsonContent []byte, err error) {
var discover Discover
discover.BaseURL = System.ServerProtocol.WEB + "://" + System.Domain
discover.DeviceAuth = System.AppName
discover.DeviceID = System.DeviceID
discover.FirmwareName = "bin_" + System.Version
discover.FirmwareVersion = System.Version
discover.FriendlyName = System.Name
discover.LineupURL = fmt.Sprintf("%s://%s/lineup.json", System.ServerProtocol.DVR, System.Domain)
discover.Manufacturer = "Golang"
discover.ModelNumber = System.Version
discover.TunerCount = Settings.Tuner
jsonContent, err = json.MarshalIndent(discover, "", " ")
return
}
func getLineupStatus() (jsonContent []byte, err error) {
var lineupStatus LineupStatus
lineupStatus.ScanInProgress = System.ScanInProgress
lineupStatus.ScanPossible = 0
lineupStatus.Source = "Cable"
lineupStatus.SourceList = []string{"IPTV", "Cable"}
jsonContent, err = json.MarshalIndent(lineupStatus, "", " ")
return
}
func getLineup() (jsonContent []byte, err error) {
var lineup Lineup
switch Settings.EpgSource {
case "PMS":
for i, dsa := range Data.Streams.Active {
var m3uChannel M3UChannelStructXEPG
err = json.Unmarshal([]byte(mapToJSON(dsa)), &m3uChannel)
if err != nil {
return
}
var stream LineupStream
stream.GuideName = m3uChannel.Name
switch len(m3uChannel.UUIDValue) {
case 0:
stream.GuideNumber = fmt.Sprintf("%d", i+1000)
guideNumber, err := getGuideNumberPMS(stream.GuideName)
if err != nil {
ShowError(err, 0)
}
stream.GuideNumber = guideNumber
default:
stream.GuideNumber = m3uChannel.UUIDValue
}
stream.URL, err = createStreamingURL("DVR", m3uChannel.FileM3UID, stream.GuideNumber, m3uChannel.Name, m3uChannel.URL)
if err == nil {
lineup = append(lineup, stream)
} else {
ShowError(err, 1202)
}
}
case "XEPG":
for _, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err != nil {
return
}
if xepgChannel.XActive == true {
var stream LineupStream
stream.GuideName = xepgChannel.XName
stream.GuideNumber = xepgChannel.XChannelID
//stream.URL = fmt.Sprintf("%s://%s/stream/%s-%s", System.ServerProtocol.DVR, System.Domain, xepgChannel.FileM3UID, base64.StdEncoding.EncodeToString([]byte(xepgChannel.URL)))
stream.URL, err = createStreamingURL("DVR", xepgChannel.FileM3UID, xepgChannel.XChannelID, xepgChannel.XName, xepgChannel.URL)
if err == nil {
lineup = append(lineup, stream)
} else {
ShowError(err, 1202)
}
}
}
}
jsonContent, err = json.MarshalIndent(lineup, "", " ")
Data.Cache.PMS = nil
saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS)
return
}
func getGuideNumberPMS(channelName string) (pmsID string, err error) {
if len(Data.Cache.PMS) == 0 {
Data.Cache.PMS = make(map[string]string)
pms, err := loadJSONFileToMap(System.File.PMS)
if err != nil {
return "", err
}
for key, value := range pms {
Data.Cache.PMS[key] = value.(string)
}
}
var getNewID = func(channelName string) (id string) {
var i int
newID:
var ids []string
id = fmt.Sprintf("id-%d", i)
for _, v := range Data.Cache.PMS {
ids = append(ids, v)
}
if indexOfString(id, ids) != -1 {
i++
goto newID
}
return
}
if value, ok := Data.Cache.PMS[channelName]; ok {
pmsID = value
} else {
pmsID = getNewID(channelName)
Data.Cache.PMS[channelName] = pmsID
saveMapToJSONFile(System.File.PMS, Data.Cache.PMS)
}
return
}

147
src/html-build.go Normal file
View File

@@ -0,0 +1,147 @@
package src
import (
"bufio"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
)
var htmlFolder string
var goFile string
var mapName string
var packageName string
var blankMap = make(map[string]interface{})
// HTMLInit : Dateipfade festlegen
// mapName = Name der zu erstellenden map
// htmlFolder: Ordner der HTML Dateien
// packageName: Name des package
func HTMLInit(name, pkg, folder, file string) {
htmlFolder = folder
goFile = file
mapName = name
packageName = pkg
}
// BuildGoFile : Erstellt das GO Dokument
func BuildGoFile() error {
var err = checkHTMLFile(htmlFolder)
if err != nil {
return err
}
var content string
content += `package ` + packageName + "\n\n"
content += `var ` + mapName + ` = make(map[string]interface{})` + "\n\n"
content += "func loadHTMLMap() {" + "\n\n"
content += createMapFromFiles(htmlFolder) + "\n"
content += "}" + "\n\n"
writeStringToFile(goFile, content)
return nil
}
// GetHTMLString : base64 -> string
func GetHTMLString(base string) string {
content, _ := base64.StdEncoding.DecodeString(base)
return string(content)
}
func createMapFromFiles(folder string) string {
var path = getLocalPath(folder)
err := filepath.Walk(path, readFilesToMap)
if err != nil {
checkErr(err)
}
var content string
for key := range blankMap {
var newKey = key
content += ` ` + mapName + `["` + newKey + `"` + `] = "` + blankMap[key].(string) + `"` + "\n"
}
return content
}
func readFilesToMap(path string, info os.FileInfo, err error) error {
if info.IsDir() == false {
var base64Str = fileToBase64(getLocalPath(path))
blankMap[path] = base64Str
}
return nil
}
func fileToBase64(file string) string {
imgFile, _ := os.Open(file)
defer imgFile.Close()
// create a new buffer base on file size
fInfo, _ := imgFile.Stat()
var size = fInfo.Size()
buf := make([]byte, int64(size))
// read file content into buffer
fReader := bufio.NewReader(imgFile)
fReader.Read(buf)
imgBase64Str := base64.StdEncoding.EncodeToString(buf)
return imgBase64Str
}
func getLocalPath(filename string) string {
path, file := filepath.Split(filename)
var newPath = filepath.Dir(path)
var newFileName = newPath + "/" + file
return newFileName
}
func writeStringToFile(filename, content string) error {
err := ioutil.WriteFile(getPlatformFile(filename), []byte(content), 0644)
if err != nil {
checkErr(err)
return err
}
return nil
}
func checkHTMLFile(filename string) error {
if _, err := os.Stat(getLocalPath(filename)); os.IsNotExist(err) {
fmt.Println(filename)
checkErr(err)
return err
}
return nil
}
func checkErr(err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
log.Println("ERROR: [", err, "] in ", file, line)
}
}

153
src/images.go Normal file
View File

@@ -0,0 +1,153 @@
package src
import (
b64 "encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
)
func getCacheImageURL(url string) (cacheImageURL string) {
url = strings.Trim(url, "\r\n")
var urlMD5 = getMD5(url)
var fileExtension = filepath.Ext(url)
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesFiles) == -1 {
Data.Cache.ImagesFiles = append(Data.Cache.ImagesFiles, urlMD5+fileExtension)
}
if Settings.CacheImages == false || System.ImageCachingInProgress == 1 {
return url
}
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesCache) != -1 {
cacheImageURL = fmt.Sprintf("%s://%s/images/%s%s", System.ServerProtocol.WEB, System.Domain, urlMD5, fileExtension)
} else {
if strings.Contains(url, System.Domain+"/images/") == false {
if indexOfString(url, Data.Cache.ImagesURLS) == -1 {
Data.Cache.ImagesURLS = append(Data.Cache.ImagesURLS, url)
}
}
cacheImageURL = url
}
return
}
func cachingImages() {
if Settings.CacheImages == false || System.ImageCachingInProgress == 1 {
return
}
System.ImageCachingInProgress = 1
showInfo("Image Caching:Images are cached")
for _, url := range Data.Cache.ImagesURLS {
if len(url) > 0 {
cacheImage(url)
}
}
showInfo("Image Caching:Done")
// Bilder die nicht mehr verwendet werden, werden gelöscht
files, err := ioutil.ReadDir(System.Folder.ImagesCache)
if err != nil {
ShowError(err, 0)
return
}
for _, file := range files {
if indexOfString(file.Name(), Data.Cache.ImagesFiles) == -1 {
var debug = fmt.Sprintf("Image Caching:Remove file: %s %s %d", System.Folder.ImagesCache+file.Name(), file.Name(), len(file.Name()))
showDebug(debug, 1)
err := os.RemoveAll(System.Folder.ImagesCache + file.Name())
if err != nil {
ShowError(err, 0)
}
}
}
System.ImageCachingInProgress = 0
return
}
func cacheImage(url string) {
var debug string
var urlMD5 = getMD5(url)
var fileExtension = filepath.Ext(url)
debug = fmt.Sprintf("Image Caching:File: %s Download: %s", urlMD5+fileExtension, url)
showDebug(debug, 1)
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return
}
var filePath = System.Folder.ImagesCache + urlMD5 + fileExtension
// Datei speichern
file, err := os.Create(filePath)
if err != nil {
return
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
return
}
func uploadLogo(input, filename string) (logoURL string, err error) {
b64data := input[strings.IndexByte(input, ',')+1:]
// BAse64 in bytes umwandeln un speichern
sDec, err := b64.StdEncoding.DecodeString(b64data)
if err != nil {
return
}
var file = fmt.Sprintf("%s%s", System.Folder.ImagesUpload, filename)
err = writeByteToFile(file, sDec)
if err != nil {
return
}
logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.WEB, System.Domain, filename)
return
}

View File

@@ -0,0 +1,592 @@
package authentication
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"time"
//"fmt"
//"log"
)
const tokenLength = 40
const saltLength = 20
const idLength = 10
var tokenValidity int
var database string
var databaseFile = "authentication.json"
var data = make(map[string]interface{})
var tokens = make(map[string]interface{})
var initAuthentication = false
// Cookie : cookie
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
}
// Framework examples
/*
func main() {
var err error
var checkErr = func(err error) {
log.Println(err)
os.Exit(0)
}
err = Init("", 10) // Path to save the data, Validity of tokens in minutes | (error)
if err != nil {
checkErr(err)
}
err = CreateDefaultUser("admin", "123")
if err != nil {
checkErr(err)
}
err = CreateNewUser("xteve", "xteve") // Username, Password | (error)
if err != nil {
checkErr(err)
}
err, token := UserAuthentication("xteve", "xteve") // Username, Password | (error, token)
if err != nil {
checkErr(err)
} else {
fmt.Println("UserAuthentication()")
fmt.Println("Token:", token)
fmt.Println("---")
}
err, newToken := CheckTheValidityOfTheToken(token) // Current token | (error, new token)
if err != nil {
checkErr(err)
} else {
fmt.Println("CheckTheValidityOfTheToken()")
fmt.Println("New Token:", newToken)
fmt.Println("---")
}
err, userID := GetUserID(newToken) // Current token | (error, user id)
if err != nil {
checkErr(err)
} else {
fmt.Println("GetUserID()")
fmt.Println("User ID:", userID)
fmt.Println("---")
}
var userData = make(map[string]interface{})
userData["type"] = "Administrator"
err = WriteUserData(userID, userData) // User id, user data | (error)
if err != nil {
checkErr(err)
}
err, userData = ReadUserData(userID) // User id | (error, userData)
if err != nil {
checkErr(err)
} else {
fmt.Println("ReadUserData()")
fmt.Println("User data:", userData)
fmt.Println("---")
}
err = RemoveUser(userID)
if err != nil {
checkErr(err)
}
}
*/
// Init : databasePath = Path to authentication.json
func Init(databasePath string, validity int) (err error) {
database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile
// Check if the database already exists
if _, err = os.Stat(database); os.IsNotExist(err) {
// Create an empty database
var defaults = make(map[string]interface{})
defaults["dbVersion"] = "1.0"
defaults["hash"] = "sha256"
defaults["users"] = make(map[string]interface{})
if saveDatabase(defaults) != nil {
return
}
}
// Loading the database
err = loadDatabase()
// Set Token Validity
tokenValidity = validity
initAuthentication = true
return
}
// CreateDefaultUser = created efault user
func CreateDefaultUser(username, password string) (err error) {
err = checkInit()
if err != nil {
return
}
var users = data["users"].(map[string]interface{})
// Check if the default user exists
if len(users) > 0 {
err = createError(001)
return
}
var defaults = defaultsForNewUser(username, password)
users[defaults["_id"].(string)] = defaults
saveDatabase(data)
return
}
// CreateNewUser : create new user
func CreateNewUser(username, password string) (userID string, err error) {
err = checkInit()
if err != nil {
return
}
var checkIfTheUserAlreadyExists = func(username string, userData map[string]interface{}) (err error) {
var salt = userData["_salt"].(string)
var loginUsername = userData["_username"].(string)
if SHA256(username, salt) == loginUsername {
err = createError(020)
}
return
}
var users = data["users"].(map[string]interface{})
for _, userData := range users {
err = checkIfTheUserAlreadyExists(username, userData.(map[string]interface{}))
if err != nil {
return
}
}
var defaults = defaultsForNewUser(username, password)
userID = defaults["_id"].(string)
users[userID] = defaults
saveDatabase(data)
return
}
// UserAuthentication : user authentication
func UserAuthentication(username, password string) (token string, err error) {
err = checkInit()
if err != nil {
return
}
var login = func(username, password string, loginData map[string]interface{}) (err error) {
err = createError(010)
var salt = loginData["_salt"].(string)
var loginUsername = loginData["_username"].(string)
var loginPassword = loginData["_password"].(string)
if SHA256(username, salt) == loginUsername {
if SHA256(password, salt) == loginPassword {
err = nil
}
}
return
}
var users = data["users"].(map[string]interface{})
for id, loginData := range users {
err = login(username, password, loginData.(map[string]interface{}))
if err == nil {
token = setToken(id, "-")
return
}
}
return
}
// CheckTheValidityOfTheToken : check token
func CheckTheValidityOfTheToken(token string) (newToken string, err error) {
err = checkInit()
if err != nil {
return
}
err = createError(011)
if v, ok := tokens[token]; ok {
var expires = v.(map[string]interface{})["expires"].(time.Time)
var userID = v.(map[string]interface{})["id"].(string)
if expires.Sub(time.Now().Local()) < 0 {
return
}
newToken = setToken(userID, token)
err = nil
} else {
return
}
return
}
// GetUserID : get user ID
func GetUserID(token string) (userID string, err error) {
err = checkInit()
if err != nil {
return
}
err = createError(002)
if v, ok := tokens[token]; ok {
var expires = v.(map[string]interface{})["expires"].(time.Time)
userID = v.(map[string]interface{})["id"].(string)
if expires.Sub(time.Now().Local()) < 0 {
return
}
err = nil
}
return
}
// WriteUserData : save user date
func WriteUserData(userID string, userData map[string]interface{}) (err error) {
err = checkInit()
if err != nil {
return
}
err = createError(030)
if v, ok := data["users"].(map[string]interface{})[userID].(map[string]interface{}); ok {
v["data"] = userData
err = saveDatabase(data)
} else {
return
}
return
}
// ReadUserData : load user date
func ReadUserData(userID string) (userData map[string]interface{}, err error) {
err = checkInit()
if err != nil {
return
}
err = createError(031)
if v, ok := data["users"].(map[string]interface{})[userID].(map[string]interface{}); ok {
userData = v["data"].(map[string]interface{})
err = nil
return
}
return
}
// RemoveUser : remove user
func RemoveUser(userID string) (err error) {
err = checkInit()
if err != nil {
return
}
err = createError(032)
if _, ok := data["users"].(map[string]interface{})[userID]; ok {
delete(data["users"].(map[string]interface{}), userID)
err = saveDatabase(data)
return
}
return
}
// SetDefaultUserData : set default user data
func SetDefaultUserData(defaults map[string]interface{}) (err error) {
allUserData, err := GetAllUserData()
for _, d := range allUserData {
var data = d.(map[string]interface{})["data"].(map[string]interface{})
var userID = d.(map[string]interface{})["_id"].(string)
for k, v := range defaults {
if _, ok := data[k]; ok {
// Key exist
} else {
data[k] = v
}
}
err = WriteUserData(userID, data)
}
return
}
// ChangeCredentials : change credentials
func ChangeCredentials(userID, username, password string) (err error) {
err = checkInit()
if err != nil {
return
}
err = createError(032)
if userData, ok := data["users"].(map[string]interface{})[userID]; ok {
//var userData = tmp.(map[string]interface{})
var salt = userData.(map[string]interface{})["_salt"].(string)
if len(username) > 0 {
userData.(map[string]interface{})["_username"] = SHA256(username, salt)
}
if len(password) > 0 {
userData.(map[string]interface{})["_password"] = SHA256(password, salt)
}
err = saveDatabase(data)
}
return
}
// GetAllUserData : get all user data
func GetAllUserData() (allUserData map[string]interface{}, err error) {
err = checkInit()
if err != nil {
return
}
if len(data) == 0 {
var defaults = make(map[string]interface{})
defaults["dbVersion"] = "1.0"
defaults["hash"] = "sha256"
defaults["users"] = make(map[string]interface{})
saveDatabase(defaults)
data = defaults
}
allUserData = data["users"].(map[string]interface{})
return
}
// CheckTheValidityOfTheTokenFromHTTPHeader : get token from HTTP header
func CheckTheValidityOfTheTokenFromHTTPHeader(w http.ResponseWriter, r *http.Request) (writer http.ResponseWriter, newToken string, err error) {
err = createError(011)
for _, cookie := range r.Cookies() {
if cookie.Name == "Token" {
var token string
token, err = CheckTheValidityOfTheToken(cookie.Value)
//fmt.Println("T", token, err)
writer = SetCookieToken(w, token)
newToken = token
}
}
//fmt.Println(err)
return
}
// Framework tools
func checkInit() (err error) {
if initAuthentication == false {
err = createError(000)
}
return
}
func saveDatabase(tmpMap interface{}) (err error) {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return
}
err = ioutil.WriteFile(database, []byte(jsonString), 0600)
if err != nil {
return
}
return
}
func loadDatabase() (err error) {
jsonString, err := ioutil.ReadFile(database)
if err != nil {
return
}
err = json.Unmarshal([]byte(jsonString), &data)
if err != nil {
return
}
return
}
// SHA256 : password + salt = sha256 string
func SHA256(secret, salt string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte("_remote_db"))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func randomString(n int) string {
const alphanum = "-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789aBcDeFgHiJkLmNoPqRsTuVwXyZ_"
var bytes = make([]byte, n)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return string(bytes)
}
func randomID(n int) string {
const alphanum = "ABCDEFGHJKLMNOPQRSTUVWXYZ0123456789"
var bytes = make([]byte, n)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return string(bytes)
}
func createError(errCode int) (err error) {
var errMsg string
switch errCode {
case 000:
errMsg = "Authentication has not yet been initialized"
case 001:
errMsg = "Default user already exists"
case 002:
errMsg = "No user id found for this token"
case 010:
errMsg = "User authentication failed"
case 011:
errMsg = "Session has expired"
case 020:
errMsg = "User already exists"
case 030:
errMsg = "User data could not be saved"
case 031:
errMsg = "User data could not be read"
case 032:
errMsg = "User ID was not found"
}
err = errors.New(errMsg)
return
}
func defaultsForNewUser(username, password string) map[string]interface{} {
var defaults = make(map[string]interface{})
var salt = randomString(saltLength)
defaults["_username"] = SHA256(username, salt)
defaults["_password"] = SHA256(password, salt)
defaults["_salt"] = salt
defaults["_id"] = "id-" + randomID(idLength)
//defaults["_one.time.token"] = randomString(tokenLength)
defaults["data"] = make(map[string]interface{})
return defaults
}
func setToken(id, oldToken string) (newToken string) {
delete(tokens, oldToken)
loopToken:
newToken = randomString(tokenLength)
if _, ok := tokens[newToken]; ok {
goto loopToken
}
var tmp = make(map[string]interface{})
tmp["id"] = id
tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
tokens[newToken] = tmp
return
}
func mapToJSON(tmpMap interface{}) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return "{}"
}
return string(jsonString)
}
// SetCookieToken : set cookie
func SetCookieToken(w http.ResponseWriter, token string) http.ResponseWriter {
expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity))
cookie := http.Cookie{Name: "Token", Value: token, Expires: expiration}
http.SetCookie(w, &cookie)
return w
}

View File

@@ -0,0 +1,84 @@
package m3u
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"testing"
)
type M3UStream struct {
GroupTitle string `json:"group-title,required"`
Name string `json:"name,required"`
TvgID string `json:"tvg-id,required"`
TvgLogo string `json:"tvg-logo,required"`
TvgName string `json:"tvg-name,required"`
URL string `json:"url,required"`
UUIDKey string `json:"_uuid.key,omitempty"`
UUIDValue string `json:"_uuid.value,omitempty"`
}
func TestStream1(t *testing.T) {
var file = "test_list_1.m3u"
var content, err = ioutil.ReadFile(file)
if err != nil {
t.Error(err)
return
}
streams, err := MakeInterfaceFromM3U(content)
if err != nil {
t.Error(err)
}
err = checkStream(streams)
if err != nil {
t.Error(err)
}
fmt.Println("Streams:", len(streams))
t.Log(streams)
}
func checkStream(streamInterface []interface{}) (err error) {
for i, s := range streamInterface {
var stream = s.(map[string]string)
var m3uStream M3UStream
jsonString, err := json.MarshalIndent(stream, "", " ")
if err == nil {
err = json.Unmarshal(jsonString, &m3uStream)
if err == nil {
log.Print(fmt.Sprintf("Stream: %d", i))
log.Print(fmt.Sprintf("Name*: %s", m3uStream.Name))
log.Print(fmt.Sprintf("URL*: %s", m3uStream.URL))
log.Print(fmt.Sprintf("tvg-name: %s", m3uStream.TvgName))
log.Print(fmt.Sprintf("tvg-id**: %s", m3uStream.TvgID))
log.Print(fmt.Sprintf("tvg-logo: %s", m3uStream.TvgLogo))
log.Print(fmt.Sprintf("group-title**: %s", m3uStream.GroupTitle))
if len(m3uStream.UUIDKey) > 0 {
log.Print(fmt.Sprintf("UUID key***: %s", m3uStream.UUIDKey))
log.Print(fmt.Sprintf("UUID value: %s", m3uStream.UUIDValue))
} else {
log.Print(fmt.Sprintf("UUID key: false"))
}
}
}
log.Println(fmt.Sprintf("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -"))
}
return
}

View File

@@ -0,0 +1,7 @@
#EXTM3U url-tvg="http://example.com/file.xml" x-tvg-url="http://example.com/xteve.xml"
#EXTINF:0 channelID="1" tvg-chno="1" tvg-name="Channel.1" tvg-id="tvg.id.1" tvg-logo="https://example/logo.png" group-title="Group 1",Channel 1
http://example.com/stream/1
#EXTINF:0 channelID="2" tvg-chno="2" tvg-name="Channel.2" tvg-id="tvg.id.2" tvg-logo="https://example/logo.png" group-title="Group 2",Channel 2
#123
http://example.com/stream/2

View File

@@ -0,0 +1,267 @@
package m3u
import (
"errors"
"net/url"
"regexp"
"strings"
)
// MakeInterfaceFromM3U :
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []interface{}, err error) {
var content = string(byteStream)
var channelName string
var parseMetaData = func(channel string) (stream map[string]string) {
stream = make(map[string]string)
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
var exceptForChannelName = `,([^\n]*|,[^\r]*)`
var lines = strings.Split(strings.Replace(channel, "\r\n", "\n", -1), "\n")
// Zeilen mit # und leerer Zeilen entfernen
for i := len(lines) - 1; i >= 0; i-- {
if len(lines[i]) == 0 || lines[i][0:1] == "#" {
lines = append(lines[:i], lines[i+1:]...)
}
}
if len(lines) >= 2 {
for _, line := range lines {
_, err := url.ParseRequestURI(line)
switch err {
case nil:
stream["url"] = strings.Trim(line, "\r\n")
default:
var value string
// Alle Parameter parsen
var p = regexp.MustCompile(exceptForParameter)
var streamParameter = p.FindAllString(line, -1)
for _, p := range streamParameter {
line = strings.Replace(line, p, "", 1)
p = strings.Replace(p, `"`, "", -1)
var parameter = strings.Split(p, "=")
if len(parameter) == 2 {
// TVG Key als Kleinbuchstaben speichern
switch strings.Contains(parameter[0], "tvg") {
case true:
stream[strings.ToLower(parameter[0])] = parameter[1]
case false:
stream[parameter[0]] = parameter[1]
}
// URL's nicht an die Filterfunktion übergeben
if !strings.Contains(parameter[1], "://") && len(parameter[1]) > 0 {
value = value + parameter[1] + " "
}
}
}
// Kanalnamen parsen
n := regexp.MustCompile(exceptForChannelName)
var name = n.FindAllString(line, 1)
if len(name) > 0 {
channelName = name[0]
channelName = strings.Replace(channelName, `,`, "", 1)
channelName = strings.TrimRight(channelName, "\r\n")
channelName = strings.TrimRight(channelName, " ")
}
if len(channelName) == 0 {
if v, ok := stream["tvg-name"]; ok {
channelName = v
}
}
channelName = strings.TrimRight(channelName, " ")
// Kanäle ohne Namen werden augelassen
if len(channelName) == 0 {
return
}
stream["name"] = channelName
value = value + channelName
stream["_values"] = value
}
}
}
// Nach eindeutiger ID im Stream suchen
for key, value := range stream {
if !strings.Contains(strings.ToLower(key), "tvg-id") {
if strings.Contains(strings.ToLower(key), "id") {
stream["_uuid.key"] = key
stream["_uuid.value"] = value
//os.Exit(0)
break
}
}
}
return
}
//fmt.Println(content)
if strings.Contains(content, "#EXTM3U") {
var channels = strings.Split(content, "#EXTINF")
channels = append(channels[:0], channels[1:]...)
for _, channel := range channels {
var stream = parseMetaData(channel)
if len(stream) > 0 && stream != nil {
allChannels = append(allChannels, stream)
}
}
} else {
err = errors.New("No valid m3u file")
}
return
}
// MakeInterfaceFromM3U2 :
func MakeInterfaceFromM3U2(byteStream []byte) (allChannels []interface{}, err error) {
var content = string(byteStream)
//var allChannels = make([]interface{}, 0)
var channels = strings.Split(content, "#EXTINF")
var parseMetaData = func(metaData string) map[string]string {
var values string // Save all values in a key
var channel = make(map[string]string)
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
//var exceptForChannelName = `(,[^.$\n]*|,[^.$\r]*)`
var exceptForChannelName = `(,[^\n]*|,[^\r]*)`
var exceptForStreamingURL = `(\n.*?\n|\r.*?\r|\n.*?\z|\r.*?\z)`
//var exceptForStreamingURL = `^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?`
// Parse all parameters
p := regexp.MustCompile(exceptForParameter)
var parameter = p.FindAllString(metaData, -1)
//fmt.Println(parameter)
for _, i := range parameter {
var remove = i
i = strings.Replace(i, `"`, "", -1)
if strings.Contains(i, "=") {
var item = strings.Split(i, "=")
switch strings.Contains(item[0], "tvg") {
case true:
channel[strings.ToLower(item[0])] = item[1]
case false:
channel[item[0]] = item[1]
}
switch strings.Contains(item[1], "://") {
case false:
values = values + item[1] + " "
}
}
metaData = strings.Replace(metaData, remove, "", 1)
}
// Parse channel name (after the comma)
n := regexp.MustCompile(exceptForChannelName)
var name = n.FindAllString(metaData, 1)
//name[len(name) - 1] = strings.Replace(name[len(name) - 1], `\r`, "", -1)
var channelName string
if len(name) == 0 {
if v, ok := channel["tvg-name"]; ok {
channelName = v
}
} else {
channelName = name[len(name)-1][1:len(name[len(name)-1])]
}
channelName = strings.Replace(channelName, `"`, "", -1)
var replacer = strings.NewReplacer("\n", "", "\r", "")
channel["name"] = replacer.Replace(channelName)
values = values + channelName + " "
// Parse streaming URL
u := regexp.MustCompile(exceptForStreamingURL)
var streamingURL = u.FindAllString(metaData, -1)
var url = strings.Replace(streamingURL[0], "\n", "", -1)
url = strings.Replace(url, "\r", "", -1)
url = strings.Trim(url, "\r\n")
channel["url"] = url
channel["_values"] = values
// Search for a unique ID
for key, value := range channel {
if !strings.Contains(strings.ToLower(key), "tvg-id") {
if strings.Contains(strings.ToLower(key), "id") {
channel["_uuid.key"] = key
channel["_uuid.value"] = value
break
}
}
}
return channel
}
if strings.Contains(channels[0], "#EXTM3U") {
for _, thisStream := range channels {
if !strings.Contains(thisStream, "#EXTM3U") {
var channel = parseMetaData(thisStream)
allChannels = append(allChannels, channel)
}
}
} else {
err = errors.New("No valid m3u file")
}
return
}

View File

@@ -0,0 +1,129 @@
package up2date
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"runtime"
"time"
)
// ClientInfo : Information about the key (NAME OS, ARCH, UUID, KEY)
type ClientInfo struct {
Arch string `json:"arch,required"`
Branch string `json:"branch,required"`
CMD string `json:"cmd,omitempty"`
Name string `json:"name,required"`
OS string `json:"os,required"`
URL string `json:"url,required"`
Response ServerResponse `json:"response,omitempty"`
}
//ServerResponse : Response from server after client request
type ServerResponse struct {
Status bool `json:"status,omitempty"`
Reason string `json:"reason,omitempty"`
Version string `json:"version,omitempty"`
UpdateBIN string `json:"update.url.bin,omitempty"`
UpdateZIP string `json:"update.url.zip,omitempty"`
Filename string `json:"filename.bin,omitempty"`
}
// Updater : Client infos
var Updater ClientInfo
// UpdateURL : URL for the new binary
var UpdateURL string
// Init : Init
func Init() {
Updater.OS = runtime.GOOS
Updater.Arch = runtime.GOARCH
}
// GetVersion : Information about the latest version
func GetVersion() (err error) {
Updater.CMD = "getVersion"
err = serverRequest()
return
}
func serverRequest() (err error) {
var serverResponse ServerResponse
jsonByte, err := json.MarshalIndent(Updater, "", " ")
if err == nil {
// Serververbindung prüfen
u, err := url.Parse(Updater.URL)
if err != nil {
return err
}
var server = u.Host
timeout := time.Duration(1 * time.Second)
_, err = net.DialTimeout("tcp", server, timeout)
if err != nil {
return err
}
// Check redirect 301 <---> 308
redirect, err := http.NewRequest("POST", Updater.URL, nil)
client := &http.Client{}
client.CheckRedirect = func(redirect *http.Request, via []*http.Request) error {
return errors.New("Redirect")
}
resp, err := client.Do(redirect)
if err != nil {
// Redirect
if resp.StatusCode >= 301 && resp.StatusCode <= 308 { //status code 301 <---> 308
Updater.URL = resp.Header.Get("Location")
} else {
return err
}
}
// ---
req, err := http.NewRequest("POST", Updater.URL, bytes.NewBuffer(jsonByte))
req.Header.Set("Content-Type", "application/json")
client = &http.Client{}
resp, err = client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
//fmt.Println(resp.StatusCode, Updater.URL, Updater.CMD)
err = fmt.Errorf(fmt.Sprintf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), Updater.URL))
return err
}
Updater.CMD = ""
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &serverResponse)
if err != nil {
return err
}
Updater.Response = serverResponse
}
return
}

View File

@@ -0,0 +1,271 @@
package up2date
import (
"archive/zip"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/kardianos/osext"
)
// DoUpdate : Update binary
func DoUpdate(fileType, filenameBIN string) (err error) {
var url string
switch fileType {
case "bin":
url = Updater.Response.UpdateBIN
case "zip":
url = Updater.Response.UpdateZIP
}
switch runtime.GOOS {
case "windows":
filenameBIN = filenameBIN + ".exe"
}
if len(url) > 0 {
log.Println("["+strings.ToUpper(fileType)+"]", "New version ("+Updater.Name+"):", Updater.Response.Version)
// Download new binary
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
log.Println("["+strings.ToUpper(fileType)+"]", "Download new version...")
if resp.StatusCode != http.StatusOK {
log.Println("["+strings.ToUpper(fileType)+"]", "Download new version...OK")
return fmt.Errorf("bad status: %s", resp.Status)
}
// Change binary filename to .filename
binary, err := osext.Executable()
var filename = getFilenameFromPath(binary)
var path = getPlatformPath(binary)
var oldBinary = path + "_old_" + filename
var newBinary = binary
// ZIP
var tmpFolder = path + "tmp"
var tmpFile = tmpFolder + string(os.PathSeparator) + filenameBIN
//fmt.Println(binary, path+"."+filename)
os.Rename(newBinary, oldBinary)
// Save the new binary with the old file name
out, err := os.Create(binary)
if err != nil {
restorOldBinary(oldBinary, newBinary)
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
restorOldBinary(oldBinary, newBinary)
return err
}
// Update as a ZIP file
if fileType == "zip" {
log.Println("["+strings.ToUpper(fileType)+"]", "Update file:", filenameBIN)
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...")
err = extractZIP(binary, tmpFolder)
binary = newBinary
if err != nil {
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...ERROR")
restorOldBinary(oldBinary, newBinary)
return err
} else {
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...OK")
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...")
err = copyFile(tmpFile, binary)
if err == nil {
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...OK")
} else {
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...ERROR")
restorOldBinary(oldBinary, newBinary)
return err
}
os.RemoveAll(tmpFolder)
}
}
// Set the permission
err = os.Chmod(binary, 0755)
// Close the new file !Windows
out.Close()
log.Println("["+strings.ToUpper(fileType)+"]", "Update Successful")
// Restart binary (Windows)
if runtime.GOOS == "windows" {
bin, err := os.Executable()
if err != nil {
restorOldBinary(oldBinary, newBinary)
return err
}
var pid = os.Getpid()
var process, _ = os.FindProcess(pid)
if proc, err := start(bin); err == nil {
os.RemoveAll(oldBinary)
process.Kill()
proc.Wait()
} else {
restorOldBinary(oldBinary, newBinary)
}
} else {
// Restart binary (Linux and UNIX)
file, _ := osext.Executable()
os.RemoveAll(oldBinary)
err = syscall.Exec(file, os.Args, os.Environ())
if err != nil {
restorOldBinary(oldBinary, newBinary)
log.Fatal(err)
return err
}
}
}
return
}
func start(args ...string) (p *os.Process, err error) {
if args[0], err = exec.LookPath(args[0]); err == nil {
//fmt.Println(args[0])
var procAttr os.ProcAttr
procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
p, err := os.StartProcess(args[0], args, &procAttr)
if err == nil {
return p, nil
}
}
return nil, err
}
func restorOldBinary(oldBinary, newBinary string) {
os.RemoveAll(newBinary)
os.Rename(oldBinary, newBinary)
}
func getPlatformFile(filename string) string {
path, file := filepath.Split(filename)
var newPath = filepath.Dir(path)
var newFileName = newPath + string(os.PathSeparator) + file
return newFileName
}
func getFilenameFromPath(path string) string {
file := filepath.Base(path)
return file
}
func getPlatformPath(path string) string {
var newPath = filepath.Dir(path) + string(os.PathSeparator)
return newPath
}
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}
func extractZIP(archive, target string) (err error) {
reader, err := zip.OpenReader(archive)
if err != nil {
return err
}
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
for _, file := range reader.File {
path := filepath.Join(target, file.Name)
if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
defer targetFile.Close()
if _, err := io.Copy(targetFile, fileReader); err != nil {
return err
}
}
return
}

238
src/m3u.go Normal file
View File

@@ -0,0 +1,238 @@
package src
import (
"encoding/json"
"fmt"
"path"
"regexp"
"sort"
"strconv"
"strings"
m3u "../src/internal/m3u-parser"
)
// Playlisten parsen
func parsePlaylist(filename, fileType string) (channels []interface{}, err error) {
content, err := readByteFromFile(filename)
var id = strings.TrimSuffix(getFilenameFromPath(filename), path.Ext(getFilenameFromPath(filename)))
var playlistName = getProviderParameter(id, fileType, "name")
if err == nil {
switch fileType {
case "m3u":
channels, err = m3u.MakeInterfaceFromM3U(content)
case "hdhr":
channels, err = makeInteraceFromHDHR(content, playlistName, id)
}
}
return
}
// Streams filtern
func filterThisStream(s interface{}) (status bool) {
status = false
var stream = s.(map[string]string)
var regexpYES = `[{]+[^.]+[}]`
var regexpNO = `!+[{]+[^.]+[}]`
for _, filter := range Data.Filter {
var group, name, search string
var exclude, include string
var match = false
var streamValues = strings.Replace(stream["_values"], "\r", "", -1)
if v, ok := stream["group-title"]; ok {
group = v
}
if v, ok := stream["name"]; ok {
name = v
}
// Unerwünschte Streams !{DEU}
r := regexp.MustCompile(regexpNO)
val := r.FindStringSubmatch(filter.Rule)
if len(val) == 1 {
exclude = val[0][2 : len(val[0])-1]
filter.Rule = strings.Replace(filter.Rule, " "+val[0], "", -1)
filter.Rule = strings.Replace(filter.Rule, val[0], "", -1)
}
// Muss zusätzlich erfüllt sein {DEU}
r = regexp.MustCompile(regexpYES)
val = r.FindStringSubmatch(filter.Rule)
if len(val) == 1 {
include = val[0][1 : len(val[0])-1]
filter.Rule = strings.Replace(filter.Rule, " "+val[0], "", -1)
filter.Rule = strings.Replace(filter.Rule, val[0], "", -1)
}
switch filter.CaseSensitive {
case false:
streamValues = strings.ToLower(streamValues)
filter.Rule = strings.ToLower(filter.Rule)
exclude = strings.ToLower(exclude)
include = strings.ToLower(include)
group = strings.ToLower(group)
name = strings.ToLower(name)
}
switch filter.Type {
case "group-title":
search = name
if group == filter.Rule {
match = true
}
case "custom-filter":
search = streamValues
if strings.Contains(search, filter.Rule) {
match = true
}
}
if match == true {
if len(exclude) > 0 {
var status = checkConditions(search, exclude, "exclude")
if status == false {
return false
}
}
if len(include) > 0 {
var status = checkConditions(search, include, "include")
if status == false {
return false
}
}
return true
}
}
return false
}
// Bedingungen für den Filter
func checkConditions(streamValues, conditions, coType string) (status bool) {
switch coType {
case "exclude":
status = true
case "include":
status = false
}
conditions = strings.Replace(conditions, ", ", ",", -1)
conditions = strings.Replace(conditions, " ,", ",", -1)
var keys = strings.Split(conditions, ",")
for _, key := range keys {
if strings.Contains(streamValues, key) {
switch coType {
case "exclude":
return false
case "include":
return true
}
}
}
return
}
// xTeVe M3U Datei erstellen
func buildM3U(groups []string) (m3u string, err error) {
var m3uChannels = make(map[float64]XEPGChannelStruct)
var channelNumbers []float64
for _, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err == nil {
if xepgChannel.XActive == true {
if len(groups) > 0 {
if indexOfString(xepgChannel.XGroupTitle, groups) == -1 {
goto Done
}
}
var channelNumber, err = strconv.ParseFloat(strings.TrimSpace(xepgChannel.XChannelID), 64)
if err == nil {
m3uChannels[channelNumber] = xepgChannel
channelNumbers = append(channelNumbers, channelNumber)
}
}
}
Done:
}
// M3U Inhalt erstellen
sort.Float64s(channelNumbers)
var xmltvURL = fmt.Sprintf("%s://%s/xmltv/xteve.xml", System.ServerProtocol.XML, System.Domain)
m3u = fmt.Sprintf(`#EXTM3U url-tvg="%s" x-tvg-url="%s"`+"\n", xmltvURL, xmltvURL)
for _, channelNumber := range channelNumbers {
var channel = m3uChannels[channelNumber]
var parameter = fmt.Sprintf(`#EXTINF:0 channelID="%s" tvg-chno="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s" group-title="%s",%s`+"\n", channel.XEPG, channel.XChannelID, channel.XName, channel.XChannelID, getCacheImageURL(channel.TvgLogo), channel.XGroupTitle, channel.XName)
var stream, err = createStreamingURL("M3U", channel.FileM3UID, channel.XChannelID, channel.XName, channel.URL)
if err == nil {
m3u = m3u + parameter + stream + "\n"
}
}
if len(groups) == 0 {
var filename = System.Folder.Data + "xteve.m3u"
err = writeByteToFile(filename, []byte(m3u))
}
return
}

84
src/maintenance.go Normal file
View File

@@ -0,0 +1,84 @@
package src
import (
"fmt"
"math/rand"
"time"
)
// InitMaintenance : Wartungsprozess initialisieren
func InitMaintenance() (err error) {
rand.Seed(time.Now().Unix())
System.TimeForAutoUpdate = fmt.Sprintf("0%d%d", randomTime(0, 2), randomTime(10, 59))
go maintenance()
return
}
func maintenance() {
for {
var t = time.Now()
// Aktualisierung der Playlist und XMLTV Dateien
if System.ScanInProgress == 0 {
for _, schedule := range Settings.Update {
if schedule == t.Format("1504") {
showInfo("Update:" + schedule)
// Backup erstellen
err := xTeVeAutoBackup()
if err != nil {
ShowError(err, 000)
}
// Playlist und XMLTV Dateien aktualisieren
getProviderData("m3u", "")
getProviderData("hdhr", "")
if Settings.EpgSource == "XEPG" {
getProviderData("xmltv", "")
}
// Datenbank für DVR erstellen
err = buildDatabaseDVR()
if err != nil {
ShowError(err, 000)
}
if Settings.CacheImages == false && System.ImageCachingInProgress == 0 {
removeChildItems(System.Folder.ImagesCache)
}
// XEPG Dateien erstellen
Data.Cache.XMLTV = make(map[string]XMLTV)
buildXEPG(false)
}
}
// Update xTeVe (Binary)
if System.TimeForAutoUpdate == t.Format("1504") {
BinaryUpdate()
}
}
time.Sleep(60 * time.Second)
}
return
}
func randomTime(min, max int) int {
rand.Seed(time.Now().Unix())
return rand.Intn(max-min) + min
}

323
src/provider.go Normal file
View File

@@ -0,0 +1,323 @@
package src
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
m3u "../src/internal/m3u-parser"
)
// fileType: Welcher Dateityp soll aktualisiert werden (m3u, hdhr, xml) | fileID: Update einer bestimmten Datei (Provider ID)
func getProviderData(fileType, fileID string) (err error) {
var fileExtension, serverFileName string
var body = make([]byte, 0)
var newProvider = false
var dataMap = make(map[string]interface{})
var saveDateFromProvider = func(fileSource, serverFileName, id string, body []byte) (err error) {
var data = make(map[string]interface{})
if value, ok := dataMap[id].(map[string]interface{}); ok {
data = value
} else {
data["id.provider"] = id
dataMap[id] = data
}
// Default keys für die Providerdaten
var keys = []string{"name", "description", "type", "file." + System.AppName, "file.source", "tuner", "last.update", "compatibility", "counter.error", "counter.download", "provider.availability"}
for _, key := range keys {
if _, ok := data[key]; !ok {
switch key {
case "name":
data[key] = serverFileName
case "description":
data[key] = ""
case "type":
data[key] = fileType
case "file." + System.AppName:
data[key] = id + fileExtension
case "file.source":
data[key] = fileSource
case "last.update":
data[key] = time.Now().Format("2006-01-02 15:04:05")
case "tuner":
if fileType == "m3u" || fileType == "hdhr" {
if _, ok := data[key].(float64); !ok {
data[key] = 1
}
}
case "compatibility":
data[key] = make(map[string]interface{})
case "counter.download":
data[key] = 0.0
case "counter.error":
data[key] = 0.0
case "provider.availability":
data[key] = 100
}
}
}
if _, ok := data["id.provider"]; !ok {
data["id.provider"] = id
}
// Datei extrahieren
body, err = extractGZIP(body, fileSource)
if err != nil {
ShowError(err, 000)
return
}
// Daten überprüfen
showInfo("Check File:" + fileSource)
switch fileType {
case "m3u":
_, err = m3u.MakeInterfaceFromM3U(body)
case "hdhr":
_, err = jsonToInterface(string(body))
case "xmltv":
err = checkXMLCompatibility(id, body)
}
if err != nil {
return
}
var filePath = System.Folder.Data + data["file."+System.AppName].(string)
err = writeByteToFile(filePath, body)
if err == nil {
data["last.update"] = time.Now().Format("2006-01-02 15:04:05")
data["counter.download"] = data["counter.download"].(float64) + 1
}
return
}
switch fileType {
case "m3u":
dataMap = Settings.Files.M3U
fileExtension = ".m3u"
case "hdhr":
dataMap = Settings.Files.HDHR
fileExtension = ".json"
case "xmltv":
dataMap = Settings.Files.XMLTV
fileExtension = ".xml"
}
for dataID, d := range dataMap {
var data = d.(map[string]interface{})
var fileSource = data["file.source"].(string)
newProvider = false
if _, ok := data["new"]; ok {
newProvider = true
delete(data, "new")
}
// Wenn eine ID vorhanden ist und nicht mit der aus der Datanbank übereinstimmt, wird die Aktualisierung übersprungen (goto)
if len(fileID) > 0 && newProvider == false {
if dataID != fileID {
goto Done
}
}
switch fileType {
case "hdhr":
// Laden vom HDHomeRun Tuner
showInfo("Tuner:" + fileSource)
var tunerURL = "http://" + fileSource + "/lineup.json"
serverFileName, body, err = downloadFileFromServer(tunerURL)
default:
if strings.Contains(fileSource, "http://") || strings.Contains(fileSource, "https://") {
// Laden vom Remote Server
showInfo("Download:" + fileSource)
serverFileName, body, err = downloadFileFromServer(fileSource)
} else {
// Laden einer lokalen Datei
showInfo("Open:" + fileSource)
err = checkFile(fileSource)
if err == nil {
body, err = readByteFromFile(fileSource)
serverFileName = getFilenameFromPath(fileSource)
}
}
}
if err == nil {
err = saveDateFromProvider(fileSource, serverFileName, dataID, body)
if err == nil {
showInfo("Save File:" + fileSource + " [ID: " + dataID + "]")
}
}
if err != nil {
ShowError(err, 000)
var downloadErr = err
if newProvider == false {
// Prüfen ob ältere Datei vorhanden ist
var file = System.Folder.Data + dataID + fileExtension
err = checkFile(file)
if err == nil {
if len(fileID) == 0 {
showWarning(1011)
}
err = downloadErr
}
// Fehler Counter um 1 erhöhen
var data = make(map[string]interface{})
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
data = value
data["counter.error"] = data["counter.error"].(float64) + 1
data["counter.download"] = data["counter.download"].(float64) + 1
}
} else {
return downloadErr
}
}
// Berechnen der Fehlerquote
if newProvider == false {
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
var data = make(map[string]interface{})
data = value
if data["counter.error"].(float64) == 0 {
data["provider.availability"] = 100
} else {
data["provider.availability"] = int(data["counter.error"].(float64)*100/data["counter.download"].(float64)*-1 + 100)
}
}
}
switch fileType {
case "m3u":
Settings.Files.M3U = dataMap
case "hdhr":
Settings.Files.HDHR = dataMap
case "xmltv":
Settings.Files.XMLTV = dataMap
delete(Data.Cache.XMLTV, System.Folder.Data+dataID+fileExtension)
}
saveSettings(Settings)
Done:
}
return
}
func downloadFileFromServer(providerURL string) (filename string, body []byte, err error) {
_, err = url.ParseRequestURI(providerURL)
if err != nil {
return
}
resp, err := http.Get(providerURL)
if err != nil {
return
}
resp.Header.Set("User-Agent", Settings.UserAgent)
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf(fmt.Sprintf("%d: %s "+http.StatusText(resp.StatusCode), resp.StatusCode, providerURL))
return
}
// Dateiname aus dem Header holen
var index = strings.Index(resp.Header.Get("Content-Disposition"), "filename")
if index > -1 {
var headerFilename = resp.Header.Get("Content-Disposition")[index:len(resp.Header.Get("Content-Disposition"))]
var value = strings.Split(headerFilename, `=`)
var f = strings.Replace(value[1], `"`, "", -1)
f = strings.Replace(f, `;`, "", -1)
filename = f
showInfo("Header filename:" + filename)
} else {
var cleanFilename = strings.SplitN(getFilenameFromPath(providerURL), "?", 2)
filename = cleanFilename[0]
}
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
return
}

404
src/screen.go Normal file
View File

@@ -0,0 +1,404 @@
package src
import (
"fmt"
"log"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
func showInfo(str string) {
var max = 22
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
if len(msg) == 2 {
for i := length; i < max; i++ {
space = space + " "
}
msg[0] = msg[0] + ":" + space
var logMsg = fmt.Sprintf("[%s] %s%s", System.Name, msg[0], msg[1])
printLogOnScreen(logMsg, "info")
logMsg = strings.Replace(logMsg, " ", "&nbsp;", -1)
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
logCleanUp()
}
return
}
func showDebug(str string, level int) {
if System.Flag.Debug < level {
return
}
var max = 22
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
var mutex = sync.RWMutex{}
if len(msg) == 2 {
for i := length; i < max; i++ {
space = space + " "
}
msg[0] = msg[0] + ":" + space
var logMsg = fmt.Sprintf("[DEBUG] %s%s", msg[0], msg[1])
printLogOnScreen(logMsg, "debug")
mutex.Lock()
logMsg = strings.Replace(logMsg, " ", "&nbsp;", -1)
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
logCleanUp()
mutex.Unlock()
}
return
}
func showHighlight(str string) {
var max = 22
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
var notification Notification
notification.Type = "info"
if len(msg) == 2 {
for i := length; i < max; i++ {
space = space + " "
}
msg[0] = msg[0] + ":" + space
var logMsg = fmt.Sprintf("[%s] %s%s", System.Name, msg[0], msg[1])
printLogOnScreen(logMsg, "highlight")
}
notification.Type = "info"
notification.Message = msg[1]
addNotification(notification)
return
}
func showWarning(errCode int) {
var errMsg = getErrMsg(errCode)
var logMsg = fmt.Sprintf("[%s] [WARNING] %s", System.Name, errMsg)
var mutex = sync.RWMutex{}
printLogOnScreen(logMsg, "warning")
mutex.Lock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
WebScreenLog.Warnings++
mutex.Unlock()
return
}
// ShowError : Zeigt die Fehlermeldungen in der Konsole
func ShowError(err error, errCode int) {
var mutex = sync.RWMutex{}
var errMsg = getErrMsg(errCode)
var logMsg = fmt.Sprintf("[%s] [ERROR] %s (%s) - EC: %d", System.Name, err, errMsg, errCode)
printLogOnScreen(logMsg, "error")
mutex.Lock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
WebScreenLog.Errors++
mutex.Unlock()
return
}
func printLogOnScreen(logMsg string, logType string) {
var color string
switch logType {
case "info":
color = "\033[0m"
case "debug":
color = "\033[35m"
case "highlight":
color = "\033[32m"
case "warning":
color = "\033[33m"
case "error":
color = "\033[31m"
}
switch runtime.GOOS {
case "windows":
log.Println(logMsg)
default:
fmt.Print(color)
log.Println(logMsg)
fmt.Print("\033[0m")
}
}
func logCleanUp() {
var logEntriesRAM = Settings.LogEntriesRAM
var logs = WebScreenLog.Log
WebScreenLog.Warnings = 0
WebScreenLog.Errors = 0
if len(logs) > logEntriesRAM {
var tmp = make([]string, 0)
for i := len(logs) - logEntriesRAM; i < logEntriesRAM; i++ {
tmp = append(tmp, logs[i])
}
logs = tmp
}
for _, log := range logs {
if strings.Contains(log, "WARNING") {
WebScreenLog.Warnings++
}
if strings.Contains(log, "ERROR") {
WebScreenLog.Errors++
}
}
WebScreenLog.Log = logs
return
}
// Fehlercodes
func getErrMsg(errCode int) (errMsg string) {
switch errCode {
case 0:
return
// Errors
case 1001:
errMsg = fmt.Sprintf("Web server could not be started.")
case 1002:
errMsg = fmt.Sprintf("No local IP address found.")
case 1003:
errMsg = fmt.Sprintf("Invalid xml")
case 1004:
errMsg = fmt.Sprintf("File not found")
case 1005:
errMsg = fmt.Sprintf("Invalide m3u")
case 1006:
errMsg = fmt.Sprintf("No playlist!")
case 1007:
errMsg = fmt.Sprintf("XEPG requires an XMLTV file.")
case 1010:
errMsg = fmt.Sprintf("Invalid file compression")
case 1011:
errMsg = fmt.Sprintf("Data is corrupt or unavailable, %s now uses an older version of this file", System.Name)
case 1012:
errMsg = fmt.Sprintf("Invalid formatting of the time")
case 1013:
errMsg = fmt.Sprintf("Invalid settings file (%s), file must be at least version %s", System.File.Settings, System.Compatibility)
case 1020:
errMsg = fmt.Sprintf("Data could not be saved, invalid keyword")
// Datenbank Update
case 1030:
errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings)
// M3U Parser
case 1050:
errMsg = fmt.Sprintf("Invalid duration specification in the M3U8 playlist.")
// M3U Parser
case 1060:
errMsg = fmt.Sprintf("Invalid characters found in the tvg parameters, streams with invalid parameters were skipped.")
// Dateisystem
case 1070:
errMsg = fmt.Sprintf("Folder could not be created.")
case 1071:
errMsg = fmt.Sprintf("File could not be created")
// Backup
case 1090:
errMsg = fmt.Sprintf("Automatic backup failed")
// Websockets
case 1100:
errMsg = fmt.Sprintf("WebUI build error")
case 1101:
errMsg = fmt.Sprintf("WebUI request error")
case 1102:
errMsg = fmt.Sprintf("WebUI response error")
// PMS Guide Numbers
case 1200:
errMsg = fmt.Sprintf("Could not create file")
// Stream URL Fehler
case 1201:
errMsg = fmt.Sprintf("Plex stream error")
case 1202:
errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist")
case 1203:
errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist")
// Warnings
case 2000:
errMsg = fmt.Sprintf("Plex can not handle more than %d streams. If you do not use Plex, you can ignore this warning.", System.DVRLimit)
case 2001:
errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.DVRLimit)
case 2002:
errMsg = fmt.Sprintf("PMS can not play m3u8 streams")
case 2003:
errMsg = fmt.Sprintf("PMS can not play streams over RTSP.")
case 2004:
errMsg = fmt.Sprintf("Buffer is disabled for this stream.")
case 2005:
errMsg = fmt.Sprintf("There are no channels mapped, use the mapping menu to assign EPG data to the channels.")
case 2010:
errMsg = fmt.Sprintf("No valid streaming URL")
case 2099:
errMsg = fmt.Sprintf("Updates have been disabled by the developer")
// Tuner
case 2105:
errMsg = fmt.Sprintf("The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again.")
case 2106:
errMsg = fmt.Sprintf("This function is only available with XEPG as EPG source")
case 2110:
errMsg = fmt.Sprintf("Don't run this as Root!")
case 2300:
errMsg = fmt.Sprintf("No channel logo found in the XMLTV or M3U file.")
case 2301:
errMsg = fmt.Sprintf("XMLTV file no longer available, channel has been deactivated.")
case 2302:
errMsg = fmt.Sprintf("Channel ID in the XMLTV file has changed. Channel has been deactivated.")
// Benutzerauthentifizierung
case 3000:
errMsg = fmt.Sprintf("Database for user authentication could not be initialized.")
case 3001:
errMsg = fmt.Sprintf("The user has no authorization to load the channels.")
// Buffer
case 4000:
errMsg = fmt.Sprintf("Connection to streaming source was interrupted.")
case 4001:
errMsg = fmt.Sprintf("Too many errors connecting to the provider. Streaming is canceled.")
case 4002:
errMsg = fmt.Sprintf("New URL for the redirect to the streaming server is missing")
case 4003:
errMsg = fmt.Sprintf("Server sends an incompatible content-type")
case 4004:
errMsg = fmt.Sprintf("This error message comes from the provider")
case 4005:
errMsg = fmt.Sprintf("Temporary buffer files could not be deleted")
// Buffer (M3U8)
case 4050:
errMsg = fmt.Sprintf("Invalid M3U8 file")
case 4051:
errMsg = fmt.Sprintf("#EXTM3U header is missing")
// Caching
case 4100:
errMsg = fmt.Sprintf("Unknown content type for downloaded image")
// API
case 5000:
errMsg = fmt.Sprintf("Invalid API command")
// Update Server
case 6001:
errMsg = fmt.Sprintf("Ivalid key")
case 6002:
errMsg = fmt.Sprintf("Update failed")
case 6003:
errMsg = fmt.Sprintf("Server not available")
case 6004:
errMsg = fmt.Sprintf("xTeVe update available")
default:
errMsg = fmt.Sprintf("Unknown error / warning (%d)", errCode)
}
return errMsg
}
func addNotification(notification Notification) (err error) {
var i int
var t = time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
notification.Time = strconv.FormatInt(t, 10)
notification.New = true
if len(notification.Headline) == 0 {
notification.Headline = strings.ToUpper(notification.Type)
}
if len(System.Notification) == 0 {
System.Notification = make(map[string]Notification)
}
System.Notification[notification.Time] = notification
for key := range System.Notification {
if i < len(System.Notification)-10 {
delete(System.Notification, key)
}
i++
}
return
}

69
src/ssdp.go Normal file
View File

@@ -0,0 +1,69 @@
package src
import (
"fmt"
"log"
"os"
"os/signal"
"time"
"github.com/koron/go-ssdp"
)
// SSDP : SSPD / DLNA Server
func SSDP() {
showInfo(fmt.Sprintf("SSDP / DLNA:%t", Settings.SSDP))
if Settings.SSDP == false {
return
}
time.Sleep(10 * time.Second)
ad, err := ssdp.Advertise(
"upnp:"+System.AppName, // send as "ST"
System.DeviceID+"::upnp:"+System.AppName, // send as "USN"
System.URLBase+"/device.xml", // send as "LOCATION"
System.AppName, // send as "SERVER"
1800) // send as "maxAge" in "CACHE-CONTROL"
if err != nil {
ShowError(err, 000)
}
// Debug SSDP
if System.Flag.Debug == 3 {
ssdp.Logger = log.New(os.Stderr, "[SSDP] ", log.LstdFlags)
}
var aliveTick <-chan time.Time
var ai = 10
if ai > 0 {
aliveTick = time.Tick(time.Duration(ai) * time.Second)
} else {
aliveTick = make(chan time.Time)
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
loop:
for {
select {
case <-aliveTick:
ad.Alive()
case <-quit:
os.Exit(0)
break loop
}
}
ad.Bye()
ad.Close()
}

109
src/struct-buffer.go Normal file
View File

@@ -0,0 +1,109 @@
package src
import "time"
// Playlist : Enthält allen Playlistinformationen, die der Buffer benötigr
type Playlist struct {
Folder string
PlaylistID string
PlaylistName string
Tuner int
Clients map[int]ThisClient
Streams map[int]ThisStream
}
// ThisClient : Clientinfos
type ThisClient struct {
Connection int
}
// ThisStream : Enthält Informationen zu dem abzuspielenden Stream einer Playlist
type ThisStream struct {
ChannelName string
Error string
Folder string
MD5 string
NetworkBandwidth int
PlaylistID string
PlaylistName string
Status bool
URL string
Segment []Segment
// Serverinformationen
Location string
URLFile string
URLHost string
URLPath string
URLRedirect string
URLScheme string
URLStreamingServer string
// Wird nur für HLS / M3U8 verwendet
Body string
Difference float64
Duration float64
DynamicBandwidth bool
FirstSequence int64
HLS bool
LastSequence int64
M3U8URL string
NewSegCount int
OldSegCount int
Sequence int64
TimeDiff float64
TimeEnd time.Time
TimeSegDuration float64
TimeStart time.Time
Version int
Wait float64
DynamicStream map[int]DynamicStream
// Lokale Temp Datein
OldSegments []string
}
// Segment : URL Segmente (HLS / M3U8)
type Segment struct {
Duration float64
Info bool
Sequence int64
URL string
Version int
Wait float64
StreamInf struct {
AverageBandwidth int
Bandwidth int
Framerate float64
Resolution string
SegmentURL string
}
}
// DynamicStream : Streaminformationen bei dynamischer Bandbreite
type DynamicStream struct {
AverageBandwidth int
Bandwidth int
Framerate float64
Resolution string
URL string
}
// ClientConnection : Client Verbindungen
type ClientConnection struct {
Connection int
Error error
}
// BandwidthCalculation : Bandbreitenberechnung für den Stream
type BandwidthCalculation struct {
NetworkBandwidth int
Size int
Start time.Time
Stop time.Time
TimeDiff float64
}

61
src/struct-hdhr.go Normal file
View File

@@ -0,0 +1,61 @@
package src
import "encoding/xml"
// Capability : HDHR Capability XML
type Capability struct {
URLBase string `xml:"URLBase"`
XMLName xml.Name `xml:"root"`
Xmlns string `xml:"xmlns,attr"`
SpecVersion struct {
Major int `xml:"major"`
Minor int `xml:"minor"`
} `xml:"specVersion"`
Device struct {
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"`
ModelName string `xml:"modelName"`
ModelNumber string `xml:"modelNumber"`
SerialNumber string `xml:"serialNumber"`
UDN string `xml:"UDN"`
} `xml:"device"`
}
// Discover : HDHR Discover /discover.json
type Discover struct {
BaseURL string `json:"BaseURL"`
DeviceAuth string `json:"DeviceAuth"`
DeviceID string `json:"DeviceID"`
FirmwareName string `json:"FirmwareName"`
FirmwareVersion string `json:"FirmwareVersion"`
FriendlyName string `json:"FriendlyName"`
LineupURL string `json:"LineupURL"`
Manufacturer string `json:"Manufacturer"`
ModelNumber string `json:"ModelNumber"`
TunerCount int `json:"TunerCount"`
}
// LineupStatus : HDHR Lineup status /lineup_status.json
type LineupStatus struct {
ScanInProgress int `json:"ScanInProgress"`
ScanPossible int `json:"ScanPossible"`
Source string `json:"Source"`
SourceList []string `json:"SourceList"`
}
// Lineup : HDHR Lineup /lineup.json
type Lineup []interface {
//GuideName string `json:"GuideName"`
//GuideNumber string `json:"GuideNumber"`
//URL string `json:"URL"`
}
// LineupStream : HDHR einzelner Stream im Lineup
type LineupStream struct {
GuideName string `json:"GuideName"`
GuideNumber string `json:"GuideNumber"`
URL string `json:"URL"`
}

280
src/struct-system.go Normal file
View File

@@ -0,0 +1,280 @@
package src
// SystemStruct : Beinhaltet alle Systeminformationen
type SystemStruct struct {
Addresses struct {
DVR string
M3U string
XML string
}
APIVersion string
AppName string
ARCH string
Branch string
Build string
Compatibility string
ConfigurationWizard bool
Dev bool
DeviceID string
Domain string
DVRLimit int
File struct {
Authentication string
M3U string
PMS string
Settings string
URLS string
XEPG string
XML string
}
Flag struct {
Branch string
Debug int
Port string
SSDP bool
}
Folder struct {
Backup string
Cache string
Config string
Data string
ImagesCache string
ImagesUpload string
Temp string
}
Hostname string
ImageCachingInProgress int
IPAddress string
IPAddressesList []string
IPAddressesV4 []string
IPAddressesV6 []string
Name string
OS string
ScanInProgress int
TimeForAutoUpdate string
Notification map[string]Notification
ServerProtocol struct {
API string
DVR string
M3U string
WEB string
XML string
}
GitHub struct {
Branch string
Repo string
Update bool
User string
}
Update struct {
Git string
Name string
}
URLBase string
Version string
WEB struct {
Menu []string
}
}
// GitStruct : Updateinformationen von GitHub
type GitStruct struct {
Filename string `json:"filename"`
Version string `json:"version"`
}
// DataStruct : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
type DataStruct struct {
Cache struct {
ImagesCache []string
ImagesFiles []string
ImagesURLS []string
PMS map[string]string
StreamingURLS map[string]StreamInfo
XMLTV map[string]XMLTV
Streams struct {
Active []string
}
}
Filter []Filter
Playlist struct {
M3U struct {
Groups struct {
Text []string
Value []string
}
}
}
StreamPreviewUI struct {
Active []string
Inactive []string
}
Streams struct {
Active []interface{}
All []interface{}
Inactive []interface{}
}
XMLTV struct {
Files []string
Mapping map[string]interface{}
}
XEPG struct {
Channels map[string]interface{}
XEPGCount int64
}
}
// Filter : Wird für die Filterregeln verwendet
type Filter struct {
CaseSensitive bool
Rule string
Type string
}
// XEPGChannelStruct : XEPG Struktur
type XEPGChannelStruct struct {
FileM3UID string `json:"_file.m3u.id,required"`
FileM3UName string `json:"_file.m3u.name,required"`
FileM3UPath string `json:"_file.m3u.path,required"`
GroupTitle string `json:"group-title,required"`
Name string `json:"name,required"`
TvgID string `json:"tvg-id,required"`
TvgLogo string `json:"tvg-logo,required"`
TvgName string `json:"tvg-name,required"`
URL string `json:"url,required"`
UUIDKey string `json:"_uuid.key,required"`
UUIDValue string `json:"_uuid.value,omitempty"`
Values string `json:"_values,required"`
XActive bool `json:"x-active,required"`
XCategory string `json:"x-category,required"`
XChannelID string `json:"x-channelID,required"`
XEPG string `json:"x-epg,required"`
XGroupTitle string `json:"x-group-title,required"`
XMapping string `json:"x-mapping,required"`
XmltvFile string `json:"x-xmltv-file,required"`
XName string `json:"x-name,required"`
XUpdateChannelIcon bool `json:"x-update-channel-icon,required"`
XUpdateChannelName bool `json:"x-update-channel-name,required"`
}
// M3UChannelStructXEPG : M3U Struktur für XEPG
type M3UChannelStructXEPG struct {
FileM3UID string `json:"_file.m3u.id,required"`
FileM3UName string `json:"_file.m3u.name,required"`
FileM3UPath string `json:"_file.m3u.path,required"`
GroupTitle string `json:"group-title,required"`
Name string `json:"name,required"`
TvgID string `json:"tvg-id,required"`
TvgLogo string `json:"tvg-logo,required"`
TvgName string `json:"tvg-name,required"`
URL string `json:"url,required"`
UUIDKey string `json:"_uuid.key,required"`
UUIDValue string `json:"_uuid.value,required"`
Values string `json:"_values,required"`
}
// FilterStruct : Filter Struktur
type FilterStruct struct {
Active bool `json:"active,required"`
CaseSensitive bool `json:"caseSensitive,required"`
Description string `json:"description,required"`
Exclude string `json:"exclude,required"`
Filter string `json:"filter,required"`
Include string `json:"include,required"`
Name string `json:"name,required"`
Rule string `json:"rule,omitempty"`
Type string `json:"type,required"`
}
// StreamingURLS : Informationen zu allen streaming URL's
type StreamingURLS struct {
Streams map[string]StreamInfo `json:"channels,required"`
}
// StreamInfo : Informationen zum Kanal für die streaming URL
type StreamInfo struct {
ChannelNumber string `json:"channelNumber,required"`
Name string `json:"name,required"`
PlaylistID string `json:"playlistID,required"`
URL string `json:"url,required"`
URLid string `json:"urlID,required"`
}
// Notification : Notifikationen im Webinterface
type Notification struct {
Headline string `json:"headline,required"`
Message string `json:"message,required"`
New bool `json:"new,required"`
Time string `json:"time,required"`
Type string `json:"type,required"`
}
// SettingsStrcut : Inhalt der settings.json
type SettingsStrcut struct {
API bool `json:"api"`
AuthenticationAPI bool `json:"authentication.api"`
AuthenticationM3U bool `json:"authentication.m3u"`
AuthenticationPMS bool `json:"authentication.pms"`
AuthenticationWEB bool `json:"authentication.web"`
AuthenticationXML bool `json:"authentication.xml"`
BackupKeep int `json:"backup.keep"`
BackupPath string `json:"backup.path"`
Branch string `json:"git.branch,omitempty"`
Buffer bool `json:"buffer"`
BufferSize int `json:"buffer.size.kb"`
BufferTimeout float64 `json:"buffer.timeout"`
CacheImages bool `json:"cache.images"`
EpgSource string `json:"epgSource"`
FileM3U []string `json:"file,omitempty"` // Beim Wizard wird die M3U in ein Slice gespeichert
FileXMLTV []string `json:"xmltv,omitempty"` // Altes Speichersystem der Provider XML Datei Slice (Wird für die Umwandlung auf das neue benötigt)
Files struct {
HDHR map[string]interface{} `json:"hdhr"`
M3U map[string]interface{} `json:"m3u"`
XMLTV map[string]interface{} `json:"xmltv"`
} `json:"files"`
FilesUpdate bool `json:"files.update"`
Filter map[int64]interface{} `json:"filter"`
Key string `json:"key,omitempty"`
Language string `json:"language"`
LogEntriesRAM int `json:"log.entries.ram"`
M3U8AdaptiveBandwidthMBPS int `json:"m3u8.adaptive.bandwidth.mbps"`
MappingFirstChannel float64 `json:"mapping.first.channel"`
Port string `json:"port"`
SSDP bool `json:"ssdp"`
TempPath string `json:"temp.path"`
Tuner int `json:"tuner"`
Update []string `json:"update"`
UpdateURL string `json:"update.url,omitempty"`
UserAgent string `json:"user.agent"`
UUID string `json:"uuid"`
Version string `json:"version"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
}
// LanguageUI : Sprache für das WebUI
type LanguageUI struct {
Login struct {
Failed string
}
}

145
src/struct-webserver.go Normal file
View File

@@ -0,0 +1,145 @@
package src
// RequestStruct : Anfragen über die Websocket Schnittstelle
type RequestStruct struct {
// Befehle an xTeVe
Cmd string `json:"cmd,required"`
// Benutzer
DeleteUser bool `json:"deleteUser,omitempty"`
UserData map[string]interface{} `json:"userData,omitempty"`
// Mapping
EpgMapping map[string]interface{} `json:"epgMapping,omitempty"`
// Restore
Base64 string `json:"base64,omitempty"`
// Neue Werte für die Einstellungen (settings.json)
Settings struct {
API *bool `json:"api,omitempty"`
AuthenticationAPI *bool `json:"authentication.api,omitempty"`
AuthenticationM3U *bool `json:"authentication.m3u,omitempty"`
AuthenticationPMS *bool `json:"authentication.pms,omitempty"`
AuthenticationWEP *bool `json:"authentication.web,omitempty"`
AuthenticationXML *bool `json:"authentication.xml,omitempty"`
BackupKeep *int `json:"backup.keep,omitempty"`
BackupPath *string `json:"backup.path,omitempty"`
Buffer *bool `json:"buffer,omitempty"`
BufferSize *int `json:"buffer.size.kb, omitempty"`
BufferTimeout *float64 `json:"buffer.timeout,omitempty"`
CacheImages *bool `json:"cache.images,omitempty"`
EpgSource *string `json:"epgSource,omitempty"`
FilesUpdate *bool `json:"files.update,omitempty"`
TempPath *string `json:"temp.path,omitempty"`
Tuner *int `json:"tuner,omitempty"`
Update *[]string `json:"update,omitempty"`
UserAgent *string `json:"user.agent,omitempty"`
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
XteveAutoUpdate *bool `json:"xteveAutoUpdate,omitempty"`
} `json:"settings,omitempty"`
// Upload Logo
Filename string `json:"filename,omitempty"`
// Filter
Filter map[int64]interface{} `json:"filter,omitempty"`
// Dateien (M3U, HDHR, XMLTV)
Files struct {
HDHR map[string]interface{} `json:"hdhr,omitempty"`
M3U map[string]interface{} `json:"m3u,omitempty"`
XMLTV map[string]interface{} `json:"xmltv,omitempty"`
} `json:"files,omitempty"`
// Wizard
Wizard struct {
EpgSource *string `json:"epgSource,omitempty"`
M3U *string `json:"m3u,omitempty"`
Tuner *int `json:"tuner,omitempty"`
XMLTV *string `json:"xmltv,omitempty"`
} `json:"wizard,omitempty"`
}
// ResponseStruct : Antworten an den Client (WEB)
type ResponseStruct struct {
ClientInfo struct {
ARCH string `json:"arch"`
Branch string `json:"branch,omitempty"`
DVR string `json:"DVR"`
EpgSource string `json:"epgSource"`
Errors int `json:"errors"`
M3U string `json:"m3u-url,required"`
OS string `json:"os"`
Streams string `json:"streams"`
UUID string `json:"uuid"`
Version string `json:"version"`
Warnings int `json:"warnings"`
XEPGCount int64 `json:"xepg"`
XML string `json:"xepg-url,required"`
} `json:"clientInfo,omitempty"`
Data struct {
Playlist struct {
M3U struct {
Groups struct {
Text []string `json:"text,required"`
Value []string `json:"value,required"`
} `json:"groups,required"`
} `json:"m3u,required"`
} `json:"playlist,required"`
StreamPreviewUI struct {
Active []string `json:"activeStreams,required"`
Inactive []string `json:"inactiveStreams,required"`
}
} `json:"data,required"`
Alert string `json:"alert,omitempty"`
ConfigurationWizard bool `json:"configurationWizard,required"`
Error string `json:"err,omitempty"`
Log WebScreenLogStruct `json:"log,required"`
LogoURL string `json:"logoURL,omitempty"`
OpenLink string `json:"openLink,omitempty"`
OpenMenu string `json:"openMenu,omitempty"`
Reload bool `json:"reload,omitempty"`
Settings SettingsStrcut `json:"settings,required"`
Status bool `json:"status,required"`
Token string `json:"token,omitempty"`
Users map[string]interface{} `json:"users,omitempty"`
Wizard int `json:"wizard,omitempty"`
XEPG map[string]interface{} `json:"xepg,required"`
Notification map[string]Notification `json:"notification,omitempty"`
}
// APIRequestStruct : Anfrage über die API Schnittstelle
type APIRequestStruct struct {
Cmd string `json:"cmd"`
Password string `json:"password"`
Token string `json:"token"`
Username string `json:"username"`
}
// APIResponseStruct : Antwort an den Client (API)
type APIResponseStruct struct {
EpgSource string `json:"epg.source,omitempty"`
Error string `json:"err,omitempty"`
Status bool `json:"status,required"`
StreamsActive int64 `json:"streams.active,omitempty"`
StreamsAll int64 `json:"streams.all,omitempty"`
StreamsXepg int64 `json:"streams.xepg,omitempty"`
Token string `json:"token,omitempty"`
URLDvr string `json:"url.dvr,omitempty"`
URLM3U string `json:"url.m3u,omitempty"`
URLXepg string `json:"url.xepg,omitempty"`
VersionAPI string `json:"version.api,omitempty"`
VersionXteve string `json:"version.xteve,omitempty"`
}
// WebScreenLogStruct : Logs werden im RAM gespeichert und für das Webinterface bereitgestellt
type WebScreenLogStruct struct {
Errors int `json:"errors,required"`
Log []string `json:"log,required"`
Warnings int `json:"warnings,required"`
}

123
src/struct-xml.go Normal file
View File

@@ -0,0 +1,123 @@
package src
import "encoding/xml"
// XMLTV : XMLTV Datei
type XMLTV struct {
Generator string `xml:"generator-info-name,attr"`
Source string `xml:"source-info-name,attr"`
XMLName xml.Name `xml:"tv"`
Channel []*Channel `xml:"channel"`
Program []*Program `xml:"programme"`
}
// Channel : Kanäle
type Channel struct {
ID string `xml:"id,attr"`
DisplayName []DisplayName `xml:"display-name"`
Icon Icon `xml:"icon"`
}
// DisplayName : Kanalname
type DisplayName struct {
Value string `xml:",chardata"`
}
// Icon : Senderlogo
type Icon struct {
Src string `xml:"src,attr"`
}
// Program : Programme
type Program struct {
Channel string `xml:"channel,attr"`
Start string `xml:"start,attr"`
Stop string `xml:"stop,attr"`
Title []*Title `xml:"title"`
SubTitle []*SubTitle `xml:"sub-title"`
Desc []*Desc `xml:"desc"`
Category []*Category `xml:"category"`
Country []*Country `xml:"country"`
EpisodeNum []*EpisodeNum `xml:"episode-num"`
Poster []Poster `xml:"icon"`
Language []*Language `xml:"language"`
Video Video `xml:"video"`
Date string `xml:"date"`
PreviouslyShown *PreviouslyShown `xml:"previously-shown"`
New *New `xml:"new"`
Live *Live `xml:"live"`
}
// Title : Programmtitel
type Title struct {
Lang string `xml:"lang,attr"`
Value string `xml:",chardata"`
}
// SubTitle : Kurzbeschreibung
type SubTitle struct {
Lang string `xml:"lang,attr"`
Value string `xml:",chardata"`
}
//Desc : Programmbeschreibung
type Desc struct {
Lang string `xml:"lang,attr"`
Value string `xml:",chardata"`
}
// Category : Kategorien
type Category struct {
Lang string `xml:"lang,attr"`
Value string `xml:",chardata"`
}
// Language : Sprachen
type Language struct {
Value string `xml:",chardata"`
}
// Country : Länder
type Country struct {
Lang string `xml:"lang,attr"`
Value string `xml:",chardata"`
}
// EpisodeNum : Episodennummerierung
type EpisodeNum struct {
System string `xml:"system,attr"`
Value string `xml:",chardata"`
}
// Poster : Programmposter / Cover
type Poster struct {
Height string `xml:"height,attr"`
Src string `xml:"src,attr"`
Value string `xml:",chardata"`
Width string `xml:"width,attr"`
}
// Video : Video Metadaten
type Video struct {
Aspect string `xml:"aspect,omitempty"`
Colour string `xml:"colour,omitempty"`
Present string `xml:"present,omitempty"`
Quality string `xml:"quality,omitempty"`
}
// PreviouslyShown : Widerholung bzw. Erstausstrahlung
type PreviouslyShown struct {
Start string `xml:"start,attr"`
}
// New : Sendung als neu deklarieren
type New struct {
Value string `xml:",chardata"`
}
// Live : Sendung als Liveübertragung deklarieren
type Live struct {
Value string `xml:",chardata"`
}

323
src/system.go Normal file
View File

@@ -0,0 +1,323 @@
package src
import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"strings"
"time"
)
// Entwicklerinfos anzeigen
func showDevInfo() {
if System.Dev == true {
fmt.Print("\033[31m")
fmt.Println("* * * * * D E V M O D E * * * * *")
fmt.Println("Version: ", System.Version)
fmt.Println("Build: ", System.Build)
fmt.Println("* * * * * * * * * * * * * * * * * *")
fmt.Print("\033[0m")
fmt.Println()
}
return
}
// Alle Systemordner erstellen
func createSystemFolders() (err error) {
e := reflect.ValueOf(&System.Folder).Elem()
for i := 0; i < e.NumField(); i++ {
var folder = e.Field(i).Interface().(string)
err = checkFolder(folder)
if err != nil {
return
}
}
return
}
// Alle Systemdateien erstellen
func createSystemFiles() (err error) {
var debug string
for _, file := range SystemFiles {
var filename = getPlatformFile(System.Folder.Config + file)
err = checkFile(filename)
if err != nil {
// Datei existiert nicht, wird jetzt erstellt
err = saveMapToJSONFile(filename, make(map[string]interface{}))
if err != nil {
return
}
debug = fmt.Sprintf("Create File:%s", filename)
showDebug(debug, 1)
}
switch file {
case "authentication.json":
System.File.Authentication = filename
case "pms.json":
System.File.PMS = filename
case "settings.json":
System.File.Settings = filename
case "xepg.json":
System.File.XEPG = filename
case "urls.json":
System.File.URLS = filename
}
}
return
}
// Einstellungen laden und default Werte setzen (xTeVe)
func loadSettings() (settings SettingsStrcut, err error) {
settingsMap, err := loadJSONFileToMap(System.File.Settings)
if err != nil {
return
}
// Deafult Werte setzten
var defaults = make(map[string]interface{})
var dataMap = make(map[string]interface{})
dataMap["xmltv"] = make(map[string]interface{})
dataMap["m3u"] = make(map[string]interface{})
dataMap["hdhr"] = make(map[string]interface{})
defaults["api"] = false
defaults["authentication.api"] = false
defaults["authentication.m3u"] = false
defaults["authentication.pms"] = false
defaults["authentication.web"] = false
defaults["authentication.xml"] = false
defaults["backup.keep"] = 10
defaults["backup.path"] = System.Folder.Backup
defaults["buffer"] = false
defaults["buffer.size.kb"] = 1024
defaults["buffer.timeout"] = 500
defaults["cache.images"] = false
defaults["epgSource"] = "XEPG"
defaults["files"] = dataMap
defaults["files.update"] = true
defaults["filter"] = make(map[string]interface{})
defaults["git.branch"] = System.Branch
defaults["language"] = "en"
defaults["log.entries.ram"] = 500
defaults["mapping.first.channel"] = 1000
defaults["xepg.replace.missing.images"] = true
defaults["m3u8.adaptive.bandwidth.mbps"] = 10
defaults["port"] = "34400"
defaults["ssdp"] = true
defaults["tuner"] = 1
defaults["update"] = []string{"0000"}
defaults["user.agent"] = System.Name
defaults["uuid"] = createUUID()
defaults["version"] = System.Version
defaults["xteveAutoUpdate"] = true
defaults["temp.path"] = System.Folder.Temp
// Default Werte setzen
for key, value := range defaults {
if _, ok := settingsMap[key]; !ok {
settingsMap[key] = value
}
}
err = json.Unmarshal([]byte(mapToJSON(settingsMap)), &settings)
if err != nil {
return
}
// Einstellungen von den Flags übernehmen
if len(System.Flag.Port) > 0 {
settings.Port = System.Flag.Port
}
if len(System.Flag.Branch) > 0 {
settings.Branch = System.Flag.Branch
showInfo(fmt.Sprintf("Git Branch:Switching Git Branch to -> %s", settings.Branch))
}
err = saveSettings(settings)
return
}
// Einstellungen speichern (xTeVe)
func saveSettings(settings SettingsStrcut) (err error) {
if settings.BackupKeep == 0 {
settings.BackupKeep = 10
}
if len(settings.BackupPath) == 0 {
settings.BackupPath = System.Folder.Backup
}
if settings.BufferTimeout < 0 {
settings.BufferTimeout = 0
}
System.Folder.Temp = settings.TempPath + settings.UUID + string(os.PathSeparator)
err = writeByteToFile(System.File.Settings, []byte(mapToJSON(settings)))
if err != nil {
return
}
Settings = settings
if System.Dev == true {
Settings.UUID = "2019-01-DEV-xTeVe!"
}
setDeviceID()
return
}
// Zugriff über die Domain ermöglichen
func setGlobalDomain(domain string) {
System.Domain = domain
switch Settings.AuthenticationPMS {
case true:
System.Addresses.DVR = "username:password@" + System.Domain
case false:
System.Addresses.DVR = System.Domain
}
switch Settings.AuthenticationM3U {
case true:
System.Addresses.M3U = System.ServerProtocol.M3U + "://" + System.Domain + "/m3u/xteve.m3u?username=xxx&password=yyy<br>(Specific groups: [http://...&group-title=foo,bar])"
case false:
System.Addresses.M3U = System.ServerProtocol.M3U + "://" + System.Domain + "/m3u/xteve.m3u (Specific groups: [http://...?group-title=foo,bar])"
}
switch Settings.AuthenticationXML {
case true:
System.Addresses.XML = System.ServerProtocol.XML + "://" + System.Domain + "/xmltv/xteve.xml?username=xxx&password=yyy"
case false:
System.Addresses.XML = System.ServerProtocol.XML + "://" + System.Domain + "/xmltv/xteve.xml"
}
if Settings.EpgSource != "XEPG" {
System.Addresses.M3U = getErrMsg(2106)
System.Addresses.XML = getErrMsg(2106)
}
return
}
// UUID generieren
func createUUID() (uuid string) {
uuid = time.Now().Format("2006-01") + "-" + randomString(4) + "-" + randomString(6)
return
}
// Eindeutige Geräte ID für Plex generieren
func setDeviceID() {
var id = Settings.UUID
switch Settings.Tuner {
case 1:
System.DeviceID = id
default:
System.DeviceID = fmt.Sprintf("%s:%d", id, Settings.Tuner)
}
return
}
// Provider Streaming-URL zu xTeVe Streaming-URL konvertieren
func createStreamingURL(streamingType, playlistID, channelNumber, channelName, url string) (streamingURL string, err error) {
var streamInfo StreamInfo
var serverProtocol string
if len(Data.Cache.StreamingURLS) == 0 {
Data.Cache.StreamingURLS = make(map[string]StreamInfo)
}
var urlID = getMD5(fmt.Sprintf("%s-%s", playlistID, url))
if s, ok := Data.Cache.StreamingURLS[urlID]; ok {
streamInfo = s
} else {
streamInfo.URL = url
streamInfo.Name = channelName
streamInfo.PlaylistID = playlistID
streamInfo.ChannelNumber = channelNumber
streamInfo.URLid = urlID
Data.Cache.StreamingURLS[urlID] = streamInfo
}
switch streamingType {
case "DVR":
serverProtocol = System.ServerProtocol.DVR
case "M3U":
serverProtocol = System.ServerProtocol.M3U
}
streamingURL = fmt.Sprintf("%s://%s/stream/%s", serverProtocol, System.Domain, streamInfo.URLid)
return
}
func getStreamInfo(urlID string) (streamInfo StreamInfo, err error) {
if len(Data.Cache.StreamingURLS) == 0 {
tmp, err := loadJSONFileToMap(System.File.URLS)
if err != nil {
return streamInfo, err
}
err = json.Unmarshal([]byte(mapToJSON(tmp)), &Data.Cache.StreamingURLS)
if err != nil {
return streamInfo, err
}
}
if s, ok := Data.Cache.StreamingURLS[urlID]; ok {
streamInfo = s
streamInfo.URL = strings.Trim(streamInfo.URL, "\r\n")
} else {
err = errors.New("Streaming error")
}
return
}

356
src/toolchain.go Normal file
View File

@@ -0,0 +1,356 @@
package src
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"os/user"
"path/filepath"
"text/template"
)
// --- System Tools ---
// Prüft ob der Ordner existiert, falls nicht, wir der Ordner erstellt
func checkFolder(path string) (err error) {
var debug string
_, err = os.Stat(filepath.Dir(path))
if os.IsNotExist(err) {
// Ordner existiert nicht, wird jetzt erstellt
err = os.MkdirAll(getPlatformPath(path), 0755)
if err == nil {
debug = fmt.Sprintf("Create Folder:%s", path)
showDebug(debug, 1)
} else {
return err
}
return nil
}
return nil
}
// Prüft ob die datei im Dateisystem existiert
func checkFile(filename string) (err error) {
var file = getPlatformFile(filename)
if _, err = os.Stat(file); os.IsNotExist(err) {
return
}
return
}
// GetUserHomeDirectory : Benutzer Homer Verzeichnis
func GetUserHomeDirectory() (userHomeDirectory string) {
usr, err := user.Current()
if err != nil {
for _, name := range []string{"HOME", "USERPROFILE"} {
if dir := os.Getenv(name); dir != "" {
userHomeDirectory = dir
break
}
}
} else {
userHomeDirectory = usr.HomeDir
}
return
}
func checkFilePermission(dir string) (err error) {
var filename = dir + "permission.test"
err = ioutil.WriteFile(filename, []byte(""), 0644)
if err == nil {
err = os.RemoveAll(filename)
}
return
}
// Ordnerpfad für das laufende OS generieren
func getPlatformPath(path string) string {
return filepath.Dir(path) + string(os.PathSeparator)
}
// Dateipfad für das laufende OS generieren
func getPlatformFile(filename string) (osFilePath string) {
path, file := filepath.Split(filename)
var newPath = filepath.Dir(path)
osFilePath = newPath + string(os.PathSeparator) + file
return
}
// Dateinamen aus dem Dateipfad ausgeben
func getFilenameFromPath(path string) (file string) {
return filepath.Base(path)
}
// Nicht mehr verwendete Systemdaten löschen
func removeOldSystemData() {
// Temporären Ordner löschen
os.RemoveAll(System.Folder.Temp)
}
//
func removeChildItems(dir string) error {
files, err := filepath.Glob(filepath.Join(dir, "*"))
if err != nil {
return err
}
for _, file := range files {
err = os.RemoveAll(file)
if err != nil {
return err
}
}
return nil
}
// JSON
func mapToJSON(tmpMap interface{}) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return "{}"
}
return string(jsonString)
}
func jsonToMap(content string) map[string]interface{} {
var tmpMap = make(map[string]interface{})
json.Unmarshal([]byte(content), &tmpMap)
return (tmpMap)
}
func jsonToMapInt64(content string) map[int64]interface{} {
var tmpMap = make(map[int64]interface{})
json.Unmarshal([]byte(content), &tmpMap)
return (tmpMap)
}
func jsonToInterface(content string) (tmpMap interface{}, err error) {
err = json.Unmarshal([]byte(content), &tmpMap)
return
}
func saveMapToJSONFile(file string, tmpMap interface{}) error {
var filename = getPlatformFile(file)
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(filename, []byte(jsonString), 0644)
if err != nil {
return err
}
return nil
}
func loadJSONFileToMap(file string) (tmpMap map[string]interface{}, err error) {
f, err := os.Open(getPlatformFile(file))
defer f.Close()
content, err := ioutil.ReadAll(f)
if err == nil {
err = json.Unmarshal([]byte(content), &tmpMap)
}
f.Close()
return
}
// Binary
func readByteFromFile(file string) (content []byte, err error) {
f, err := os.Open(getPlatformFile(file))
defer f.Close()
content, err = ioutil.ReadAll(f)
f.Close()
return
}
func writeByteToFile(file string, data []byte) (err error) {
var filename = getPlatformFile(file)
err = ioutil.WriteFile(filename, data, 0644)
return
}
func readStringFromFile(file string) (str string, err error) {
var content []byte
var filename = getPlatformFile(file)
err = checkFile(filename)
if err != nil {
return
}
content, err = ioutil.ReadFile(filename)
if err != nil {
ShowError(err, 0)
return
}
str = string(content)
return
}
// Netzwerk
func resolveHostIP() (err error) {
netInterfaceAddresses, err := net.InterfaceAddrs()
if err != nil {
return
}
for _, netInterfaceAddress := range netInterfaceAddresses {
networkIP, ok := netInterfaceAddress.(*net.IPNet)
System.IPAddressesList = append(System.IPAddressesList, networkIP.IP.String())
if ok {
var ip = networkIP.IP.String()
if networkIP.IP.To4() != nil {
System.IPAddressesV4 = append(System.IPAddressesV4, ip)
if !networkIP.IP.IsLoopback() && ip[0:7] != "169.254" {
System.IPAddress = ip
}
} else {
System.IPAddressesV6 = append(System.IPAddressesV6, ip)
}
}
}
System.Hostname, err = os.Hostname()
if err != nil {
return
}
return
}
// Sonstiges
func randomString(n int) string {
const alphanum = "AB1CD2EF3GH4IJ5KL6MN7OP8QR9ST0UVWXYZ"
var bytes = make([]byte, n)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = alphanum[b%byte(len(alphanum))]
}
return string(bytes)
}
func parseTemplate(content string, tmpMap map[string]interface{}) (result string) {
t := template.Must(template.New("template").Parse(content))
var tpl bytes.Buffer
if err := t.Execute(&tpl, tmpMap); err != nil {
ShowError(err, 0)
}
result = tpl.String()
return
}
func indexOfString(element string, data []string) int {
for k, v := range data {
if element == v {
return k
}
}
return -1
}
func indexOfFloat64(element float64, data []float64) int {
for k, v := range data {
if element == v {
return (k)
}
}
return -1
}
func indexOfInt(element int, data []int) int {
for k, v := range data {
if element == v {
return (k)
}
}
return -1
}
func getMD5(str string) string {
md5Hasher := md5.New()
md5Hasher.Write([]byte(str))
return hex.EncodeToString(md5Hasher.Sum(nil))
}

273
src/update.go Normal file
View File

@@ -0,0 +1,273 @@
package src
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
up2date "../src/internal/up2date/client"
"reflect"
)
// BinaryUpdate : Binary Update Prozess. Git Branch master und beta wird von GitHub geladen.
func BinaryUpdate() (err error) {
if System.GitHub.Update == false {
showWarning(2099)
return
}
var debug string
var updater = &up2date.Updater
updater.Name = System.Update.Name
updater.Branch = System.Branch
up2date.Init()
switch System.Branch {
// Update von GitHub
case "master", "beta":
var gitInfo = fmt.Sprintf("%s/%s/info.json?raw=true", System.Update.Git, System.Branch)
var zipFile = fmt.Sprintf("%s/%s/%s_%s_%s.zip?raw=true", System.Update.Git, System.Branch, System.AppName, System.OS, System.ARCH)
var body []byte
var git GitStruct
resp, err := http.Get(gitInfo)
if err != nil {
ShowError(err, 0)
return err
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == 404 {
err = fmt.Errorf(fmt.Sprintf("Update Server: %s (%s)", http.StatusText(resp.StatusCode), gitInfo))
ShowError(err, 6003)
return nil
}
err = fmt.Errorf(fmt.Sprintf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), gitInfo))
return err
}
body, err = ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &git)
if err != nil {
return err
}
updater.Response.Status = true
updater.Response.UpdateZIP = zipFile
updater.Response.Version = git.Version
updater.Response.Filename = git.Filename
// Update vom eigenen Server
default:
updater.URL = Settings.UpdateURL
if len(updater.URL) == 0 {
showInfo(fmt.Sprintf("Update URL:No server URL specified, update will not be performed. Branch: %s", System.Branch))
return
}
showInfo("Update URL:" + updater.URL)
fmt.Println("-----------------")
// Versionsinformationen vom Server laden
err = up2date.GetVersion()
if err != nil {
debug = fmt.Sprintf(err.Error())
showDebug(debug, 1)
return nil
}
if len(updater.Response.Reason) > 0 {
err = fmt.Errorf(fmt.Sprintf("Update Server: %s", updater.Response.Reason))
ShowError(err, 6002)
return nil
}
}
var currentVersion = System.Version + "." + System.Build
// Versionsnummer überprüfen
if updater.Response.Version > currentVersion && updater.Response.Status == true {
if Settings.XteveAutoUpdate == true {
// Update durchführen
var fileType, url string
showInfo(fmt.Sprintf("Update Available:Version: %s", updater.Response.Version))
switch System.Branch {
// Update von GitHub
case "master", "beta":
showInfo(fmt.Sprintf("Update Server:GitHub"))
// Update vom eigenen Server
default:
showInfo(fmt.Sprintf("Update Server:%s", Settings.UpdateURL))
}
showInfo(fmt.Sprintf("Start Update:Branch: %s", updater.Branch))
// Neue Version als BIN Datei herunterladen
if len(updater.Response.UpdateBIN) > 0 {
url = updater.Response.UpdateBIN
fileType = "bin"
}
// Neue Version als ZIP Datei herunterladen
if len(updater.Response.UpdateZIP) > 0 {
url = updater.Response.UpdateZIP
fileType = "zip"
}
if len(url) > 0 {
err = up2date.DoUpdate(fileType, updater.Response.Filename)
if err != nil {
ShowError(err, 6002)
}
}
} else {
// Hinweis ausgeben
showWarning(6004)
}
}
return nil
}
func conditionalUpdateChanges() (err error) {
checkVersion:
settingsMap, err := loadJSONFileToMap(System.File.Settings)
if err != nil || len(settingsMap) == 0 {
return
}
if settingsVersion, ok := settingsMap["version"].(string); ok {
// Letzte Kompatible Version (1.4.4)
if settingsVersion < System.Compatibility {
err = errors.New(getErrMsg(1013))
return
}
switch settingsVersion {
case "1.4.4":
// UUID Wert in xepg.json setzen
err = setValueForUUID()
if err != nil {
return
}
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
if oldFilter, ok := settingsMap["filter"].([]interface{}); ok {
var newFilterMap = convertToNewFilter(oldFilter)
settingsMap["filter"] = newFilterMap
settingsMap["version"] = "1.9.0"
err = saveMapToJSONFile(System.File.Settings, settingsMap)
if err != nil {
return
}
goto checkVersion
} else {
err = errors.New(getErrMsg(1030))
return
}
case "1.9.0":
// Falls es in einem späteren Update Änderungen an der Datenbank gibt, geht es hier weiter
break
}
} else {
// settings.json ist zu alt (älter als Version 1.4.4)
err = errors.New(getErrMsg(1013))
}
return
}
func convertToNewFilter(oldFilter []interface{}) (newFilterMap map[int]interface{}) {
newFilterMap = make(map[int]interface{})
switch reflect.TypeOf(oldFilter).Kind() {
case reflect.Slice:
s := reflect.ValueOf(oldFilter)
for i := 0; i < s.Len(); i++ {
var newFilter FilterStruct
newFilter.Active = true
newFilter.Name = fmt.Sprintf("Custom filter %d", i+1)
newFilter.Filter = s.Index(i).Interface().(string)
newFilter.Type = "custom-filter"
newFilter.CaseSensitive = false
newFilterMap[i] = newFilter
}
}
return
}
func setValueForUUID() (err error) {
xepg, err := loadJSONFileToMap(System.File.XEPG)
for _, c := range xepg {
var xepgChannel = c.(map[string]interface{})
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {
if value, ok := xepgChannel[uuidKey].(string); ok {
if len(value) > 0 {
xepgChannel["_uuid.value"] = value
}
}
}
}
err = saveMapToJSONFile(System.File.XEPG, xepg)
return
}

54
src/webUI.go Normal file

File diff suppressed because one or more lines are too long

1050
src/webserver.go Normal file

File diff suppressed because it is too large Load Diff

967
src/xepg.go Normal file
View File

@@ -0,0 +1,967 @@
package src
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io/ioutil"
"path"
"runtime"
"strconv"
"strings"
"time"
)
// Provider XMLTV Datei überprüfen
func checkXMLCompatibility(id string, body []byte) (err error) {
var xmltv XMLTV
var compatibility = make(map[string]int)
err = xml.Unmarshal(body, &xmltv)
if err != nil {
return
}
compatibility["xmltv.channels"] = len(xmltv.Channel)
compatibility["xmltv.programs"] = len(xmltv.Program)
setProviderCompatibility(id, "xmltv", compatibility)
return
}
// XEPG Daten erstellen
func buildXEPG(background bool) {
if System.ScanInProgress == 1 {
return
}
System.ScanInProgress = 1
if Settings.EpgSource == "XEPG" {
switch background {
case true:
go func() {
createXEPGMapping()
createXEPGDatabase()
mapping()
cleanupXEPG()
createXMLTVFile()
createM3UFile()
go cachingImages()
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0
// Cache löschen
/*
Data.Cache.XMLTV = make(map[string]XMLTV)
Data.Cache.XMLTV = nil
*/
runtime.GC()
}()
case false:
createXEPGMapping()
createXEPGDatabase()
mapping()
cleanupXEPG()
go func() {
createXMLTVFile()
createM3UFile()
go cachingImages()
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0
// Cache löschen
//Data.Cache.XMLTV = make(map[string]XMLTV)
//Data.Cache.XMLTV = nil
runtime.GC()
}()
}
} else {
getLineup()
System.ScanInProgress = 0
}
}
// XEPG Daten aktualisieren
func updateXEPG(background bool) {
if System.ScanInProgress == 1 {
return
}
System.ScanInProgress = 1
if Settings.EpgSource == "XEPG" {
switch background {
case false:
createXEPGDatabase()
mapping()
cleanupXEPG()
go func() {
createXMLTVFile()
createM3UFile()
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0
}()
case true:
System.ScanInProgress = 0
}
} else {
System.ScanInProgress = 0
}
// Cache löschen
//Data.Cache.XMLTV = nil //make(map[string]XMLTV)
//Data.Cache.XMLTV = make(map[string]XMLTV)
return
}
// Mapping Menü für die XMLTV Dateien erstellen
func createXEPGMapping() {
Data.XMLTV.Files = getLocalProviderFiles("xmltv")
Data.XMLTV.Mapping = make(map[string]interface{})
var tmpMap = make(map[string]interface{})
var friendlyDisplayName = func(channel Channel) (displayName string) {
var dn = channel.DisplayName
displayName = dn[0].Value
switch len(dn) {
case 1:
displayName = dn[0].Value
default:
displayName = fmt.Sprintf("%s (%s)", dn[1].Value, dn[0].Value)
}
return
}
if len(Data.XMLTV.Files) == 0 {
return
}
for i := len(Data.XMLTV.Files) - 1; i >= 0; i-- {
var file = Data.XMLTV.Files[i]
var err error
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file)))
showInfo("XEPG:" + "Parse XMLTV file: " + getProviderParameter(fileID, "xmltv", "name"))
//xmltv, err = getLocalXMLTV(file)
var xmltv XMLTV
err = getLocalXMLTV(file, &xmltv)
if err != nil {
Data.XMLTV.Files = append(Data.XMLTV.Files, Data.XMLTV.Files[i+1:]...)
var errMsg = err.Error()
err = errors.New(getProviderParameter(fileID, "xmltv", "name") + ": " + errMsg)
ShowError(err, 000)
}
// XML Parsen (Provider Datei)
if err == nil {
// Daten aus der XML Datei in eine temporäre Map schreiben
var xmltvMap = make(map[string]interface{})
for _, c := range xmltv.Channel {
var channel = make(map[string]interface{})
channel["id"] = c.ID
channel["display-name"] = friendlyDisplayName(*c)
channel["icon"] = c.Icon.Src
xmltvMap[c.ID] = channel
}
tmpMap[getFilenameFromPath(file)] = xmltvMap
Data.XMLTV.Mapping[getFilenameFromPath(file)] = xmltvMap
}
}
// Auswahl für den Dummy erstellen
var dummy = make(map[string]interface{})
var times = []string{"30", "60", "90", "120"}
for _, i := range times {
var dummyChannel = make(map[string]string)
dummyChannel["display-name"] = i + " Minutes"
dummyChannel["id"] = i + "_Minutes"
dummyChannel["icon"] = ""
dummy[dummyChannel["id"]] = dummyChannel
}
Data.XMLTV.Mapping = tmpMap
Data.XMLTV.Mapping["xTeVe Dummy"] = dummy
tmpMap = make(map[string]interface{})
return
}
// XEPG Datenbank erstellen / aktualisieren
func createXEPGDatabase() (err error) {
var allChannelNumbers []float64
Data.Cache.Streams.Active = []string{}
Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG)
if err != nil {
ShowError(err, 1004)
return err
}
var createNewID = func() (xepg string) {
var firstID = 0 //len(Data.XEPG.Channels)
newXEPGID:
if _, ok := Data.XEPG.Channels["x-ID."+strconv.FormatInt(int64(firstID), 10)]; ok {
firstID++
goto newXEPGID
}
xepg = "x-ID." + strconv.FormatInt(int64(firstID), 10)
return
}
var getFreeChannelNumber = func() (xChannelID string) {
var firstFreeNumber float64 = Settings.MappingFirstChannel
newNumber:
if indexOfFloat64(firstFreeNumber, allChannelNumbers) == -1 {
xChannelID = fmt.Sprintf("%g", firstFreeNumber)
allChannelNumbers = append(allChannelNumbers, firstFreeNumber)
} else {
firstFreeNumber++
goto newNumber
}
return
}
showInfo("XEPG:" + "Update database")
// Kanal mit fehlenden Kanalnummern löschen
for id, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err != nil {
return
}
if len(xepgChannel.XChannelID) == 0 {
fmt.Println(mapToJSON(xepgChannel))
delete(Data.XEPG.Channels, id)
}
if xChannelID, err := strconv.ParseFloat(xepgChannel.XChannelID, 64); err == nil {
allChannelNumbers = append(allChannelNumbers, xChannelID)
}
}
for _, dsa := range Data.Streams.Active {
var channelExists = false // Entscheidet ob ein Kanal neu zu Datenbank hinzugefügt werden soll.
var channelHasUUID = false // Überprüft, ob der Kanal (Stream) eindeutige ID's besitzt
var currentXEPGID string // Aktuelle Datenbank ID (XEPG). Wird verwendet, um den Kanal in der Datenbank mit dem Stream der M3u zu aktualisieren
var m3uChannel M3UChannelStructXEPG
err = json.Unmarshal([]byte(mapToJSON(dsa)), &m3uChannel)
if err != nil {
return
}
Data.Cache.Streams.Active = append(Data.Cache.Streams.Active, m3uChannel.Name)
// XEPG Datenbank durchlaufen um nach dem Kanal zu suchen.
for xepg, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err != nil {
return
}
// Vergleichen des Streams anhand einer UUID in der M3U mit dem Kanal in der Databank
if len(xepgChannel.UUIDValue) > 0 && len(m3uChannel.UUIDValue) > 0 {
if xepgChannel.UUIDValue == m3uChannel.UUIDValue && xepgChannel.UUIDKey == m3uChannel.UUIDKey {
channelExists = true
channelHasUUID = true
currentXEPGID = xepg
break
}
} else {
// Vergleichen des Streams mit dem Kanal in der Databank anhand des Kanalnamens
//fmt.Println(xepgChannel.Name, xepgChannel.UUIDKey, xepgChannel.UUIDValue)
if xepgChannel.Name == m3uChannel.Name {
channelExists = true
currentXEPGID = xepg
break
}
}
}
//os.Exit(0)
switch channelExists {
case true:
// Bereits vorhandener Kanal
var xepgChannel XEPGChannelStruct
err = json.Unmarshal([]byte(mapToJSON(Data.XEPG.Channels[currentXEPGID])), &xepgChannel)
if err != nil {
return
}
// Streaming URL aktualisieren
xepgChannel.URL = m3uChannel.URL
// Name aktualisieren, anhand des Names wird überprüft ob der Kanal noch in einer Playlist verhanden. Funktion: cleanupXEPG
xepgChannel.Name = m3uChannel.Name
// Kanalname aktualisieren, nur mit Kanal ID's möglich
if channelHasUUID == true {
if xepgChannel.XUpdateChannelName == true {
xepgChannel.XName = m3uChannel.Name
}
}
// Kanallogo aktualisieren. Wird bei vorhandenem Logo in der XMLTV Datei wieder überschrieben
if xepgChannel.XUpdateChannelIcon == true {
xepgChannel.TvgLogo = m3uChannel.TvgLogo
}
Data.XEPG.Channels[currentXEPGID] = xepgChannel
case false:
// Neuer Kanal
var xepg = createNewID()
var xChannelID = getFreeChannelNumber()
var newChannel XEPGChannelStruct
newChannel.FileM3UID = m3uChannel.FileM3UID
newChannel.FileM3UName = m3uChannel.FileM3UName
newChannel.FileM3UPath = m3uChannel.FileM3UPath
newChannel.Values = m3uChannel.Values
newChannel.GroupTitle = m3uChannel.GroupTitle
newChannel.Name = m3uChannel.Name
newChannel.TvgID = m3uChannel.TvgID
newChannel.TvgLogo = m3uChannel.TvgLogo
newChannel.TvgName = m3uChannel.TvgName
newChannel.URL = m3uChannel.URL
newChannel.XmltvFile = ""
newChannel.XMapping = ""
if len(m3uChannel.UUIDKey) > 0 {
newChannel.UUIDKey = m3uChannel.UUIDKey
newChannel.UUIDValue = m3uChannel.UUIDValue
}
newChannel.XName = m3uChannel.Name
newChannel.XGroupTitle = m3uChannel.GroupTitle
newChannel.XEPG = xepg
newChannel.XChannelID = xChannelID
Data.XEPG.Channels[xepg] = newChannel
}
}
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
if err != nil {
return
}
return
}
// Kanäle automatisch zuordnen und das Mapping überprüfen
func mapping() (err error) {
showInfo("XEPG:" + "Map channels")
for xepg, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err != nil {
return
}
// Automatische Mapping für neue Kanäle. Wird nur ausgeführt, wenn der Kanal deaktiviert ist und keine XMLTV Datei und kein XMLTV Kanal zugeordnet ist.
if xepgChannel.XActive == false {
// Werte kann "-" sein, deswegen len < 1
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XmltvFile) < 1 {
var tvgID = xepgChannel.TvgID
// Default für neuen Kanal setzen
xepgChannel.XmltvFile = "-"
xepgChannel.XMapping = "-"
Data.XEPG.Channels[xepg] = xepgChannel
for file, xmltvChannels := range Data.XMLTV.Mapping {
if channel, ok := xmltvChannels.(map[string]interface{})[tvgID]; ok {
if channelID, ok := channel.(map[string]interface{})["id"].(string); ok {
xepgChannel.XmltvFile = file
xepgChannel.XMapping = channelID
xepgChannel.XActive = true
// Falls in der XMLTV Datei ein Logo existiert, wird dieses verwendet. Falls nicht, dann das Logo aus der M3U Datei
if icon, ok := channel.(map[string]interface{})["icon"].(string); ok {
if len(icon) > 0 {
xepgChannel.TvgLogo = icon
}
}
Data.XEPG.Channels[xepg] = xepgChannel
break
}
}
}
}
}
// Überprüfen, ob die zugeordneten XMLTV Dateien und Kanäle noch existieren.
if xepgChannel.XActive == true {
var mapping = xepgChannel.XMapping
var file = xepgChannel.XmltvFile
if file != "xTeVe Dummy" {
if value, ok := Data.XMLTV.Mapping[file].(map[string]interface{}); ok {
if channel, ok := value[mapping].(map[string]interface{}); ok {
// Kanallogo aktualisieren
if logo, ok := channel["icon"].(string); ok {
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 {
xepgChannel.TvgLogo = logo
}
}
} else {
ShowError(fmt.Errorf(fmt.Sprintf("Missing EPG data: %s", xepgChannel.Name)), 0)
showWarning(2302)
xepgChannel.XActive = false
}
} else {
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file)))
ShowError(fmt.Errorf("Missing XMLTV file: %s", getProviderParameter(fileID, "xmltv", "name")), 0)
showWarning(2301)
xepgChannel.XActive = false
}
}
if len(xepgChannel.XmltvFile) == 0 {
xepgChannel.XmltvFile = "-"
xepgChannel.XActive = false
}
if len(xepgChannel.XMapping) == 0 {
xepgChannel.XMapping = "-"
xepgChannel.XActive = false
}
Data.XEPG.Channels[xepg] = xepgChannel
}
}
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
if err != nil {
return
}
return
}
// XMLTV Datei erstellen
func createXMLTVFile() (err error) {
Data.Cache.ImagesFiles = []string{}
Data.Cache.ImagesURLS = []string{}
Data.Cache.ImagesCache = []string{}
files, err := ioutil.ReadDir(System.Folder.ImagesCache)
if err == nil {
for _, file := range files {
if indexOfString(file.Name(), Data.Cache.ImagesCache) == -1 {
Data.Cache.ImagesCache = append(Data.Cache.ImagesCache, file.Name())
}
}
}
if len(Data.XMLTV.Files) == 0 && len(Data.Streams.Active) == 0 {
Data.XEPG.Channels = make(map[string]interface{})
return
}
showInfo("XEPG:" + fmt.Sprintf("Create XMLTV file (%s)", System.File.XML))
var xepgXML XMLTV
xepgXML.Generator = System.Name
xepgXML.Source = fmt.Sprintf("%s - %s", System.Name, System.Version)
var tmpProgram = &XMLTV{}
for _, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err == nil {
if xepgChannel.XActive == true {
// Kanäle
var channel Channel
channel.ID = xepgChannel.XChannelID
channel.Icon = Icon{Src: getCacheImageURL(xepgChannel.TvgLogo)}
channel.DisplayName = append(channel.DisplayName, DisplayName{Value: xepgChannel.XName})
xepgXML.Channel = append(xepgXML.Channel, &channel)
// Programme
*tmpProgram, err = getProgramData(xepgChannel)
if err == nil {
for _, program := range tmpProgram.Program {
xepgXML.Program = append(xepgXML.Program, program)
}
}
}
}
}
var content, _ = xml.MarshalIndent(xepgXML, " ", " ")
var xmlOutput = []byte(xml.Header + string(content))
writeByteToFile(System.File.XML, xmlOutput)
xepgXML = XMLTV{}
//saveMapToJSONFile(System.File.Images, Data.Cache.ImageCache)
return
}
// Programmdaten erstellen (createXMLTVFile)
func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile
var channelID = xepgChannel.XMapping
var xmltv XMLTV
if xmltvFile == System.Folder.Data+"xTeVe Dummy" {
xmltv = createDummyProgram(xepgChannel)
} else {
err = getLocalXMLTV(xmltvFile, &xmltv)
if err != nil {
return
}
}
for _, xmltvProgram := range xmltv.Program {
if xmltvProgram.Channel == channelID {
//fmt.Println(&channelID)
var program = &Program{}
// Channel ID
program.Channel = xepgChannel.XChannelID
program.Start = xmltvProgram.Start
program.Stop = xmltvProgram.Stop
// Title
program.Title = xmltvProgram.Title
// Sub title (Untertitel)
program.SubTitle = xmltvProgram.SubTitle
// Description (Beschreibung)
program.Desc = xmltvProgram.Desc
// Category (Kategorie)
getCategory(program, xmltvProgram, xepgChannel)
// Country (Länder)
program.Country = xmltvProgram.Country
// Program icon (Poster / Cover)
getPoster(program, xmltvProgram, xepgChannel)
// Language (Sprache)
program.Language = xmltvProgram.Language
// Episodes numbers (Episodennummern)
getEpisodeNum(program, xmltvProgram, xepgChannel)
// Video (Videoparameter)
getVideo(program, xmltvProgram, xepgChannel)
// Date (Datum)
program.Date = xmltvProgram.Date
// Previously shown (Wiederholung)
program.PreviouslyShown = xmltvProgram.PreviouslyShown
// New (Neu)
program.New = xmltvProgram.New
// Live
program.Live = xmltvProgram.Live
xepgXML.Program = append(xepgXML.Program, program)
}
}
return
}
// Dummy Daten erstellen (createXMLTVFile)
func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
var currentTime = time.Now()
var dateArray = strings.Fields(currentTime.String())
var offset = " " + dateArray[2]
var currentDay = currentTime.Format("20060102")
var startTime, _ = time.Parse("20060102150405", currentDay+"000000")
showInfo("Create Dummy Guide:" + "Time offset" + offset + " - " + xepgChannel.XName)
var dl = strings.Split(xepgChannel.XMapping, "_")
dummyLength, err := strconv.Atoi(dl[0])
if err != nil {
ShowError(err, 000)
return
}
for d := 0; d < 4; d++ {
var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24))
for t := dummyLength; t <= 1440; t = t + dummyLength {
var epgStopTime = epgStartTime.Add(time.Minute * time.Duration(dummyLength))
var epg Program
poster := Poster{}
epg.Channel = xepgChannel.XMapping
epg.Start = epgStartTime.Format("20060102150405") + offset
epg.Stop = epgStopTime.Format("20060102150405") + offset
epg.Title = append(epg.Title, &Title{Value: xepgChannel.XName + " (" + epgStartTime.Weekday().String()[0:2] + ". " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04") + ")", Lang: "en"})
epg.Desc = append(epg.Desc, &Desc{Value: "xTeVe: (" + strconv.Itoa(dummyLength) + " Minutes) " + epgStartTime.Weekday().String() + " " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04"), Lang: "en"})
if Settings.XepgReplaceMissingImages == true {
poster.Src = getCacheImageURL(xepgChannel.TvgLogo)
epg.Poster = append(epg.Poster, poster)
}
epg.EpisodeNum = append(epg.EpisodeNum, &EpisodeNum{Value: epgStartTime.Format("2006-01-02 15:04:05"), System: "original-air-date"})
epg.New = &New{Value: ""}
dummyXMLTV.Program = append(dummyXMLTV.Program, &epg)
epgStartTime = epgStopTime
}
}
return
}
// Kategorien erweitern (createXMLTVFile)
func getCategory(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) {
for _, i := range xmltvProgram.Category {
category := &Category{}
category.Value = i.Value
category.Lang = i.Lang
program.Category = append(program.Category, category)
}
if len(xepgChannel.XCategory) > 0 {
category := &Category{}
category.Value = xepgChannel.XCategory
category.Lang = "en"
program.Category = append(program.Category, category)
}
return
}
// Programm Poster Cover aus der XMLTV Datei laden
func getPoster(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) {
for _, poster := range xmltvProgram.Poster {
poster.Src = getCacheImageURL(poster.Src)
program.Poster = append(program.Poster, poster)
}
if Settings.XepgReplaceMissingImages == true {
if len(xmltvProgram.Poster) == 0 {
var poster Poster
poster.Src = getCacheImageURL(xepgChannel.TvgLogo)
program.Poster = append(program.Poster, poster)
}
}
}
// Episodensystem übernehmen, falls keins vorhanden ist und eine Kategorie im Mapping eingestellt wurden, wird eine Episode erstellt
func getEpisodeNum(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) {
program.EpisodeNum = xmltvProgram.EpisodeNum
if len(xepgChannel.XCategory) > 0 {
if len(xmltvProgram.EpisodeNum) == 0 {
program.EpisodeNum = append(program.EpisodeNum, &EpisodeNum{Value: time.Now().Format("2006-01-02"), System: "original-air-date"})
}
}
return
}
// Videoparameter erstellen (createXMLTVFile)
func getVideo(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) {
var video Video
video.Present = xmltvProgram.Video.Present
video.Colour = xmltvProgram.Video.Colour
video.Aspect = xmltvProgram.Video.Aspect
video.Quality = xmltvProgram.Video.Quality
if len(xmltvProgram.Video.Quality) == 0 {
if strings.Contains(strings.ToUpper(xepgChannel.XName), " HD") || strings.Contains(strings.ToUpper(xepgChannel.XName), " FHD") {
video.Quality = "HDTV"
}
if strings.Contains(strings.ToUpper(xepgChannel.XName), " UHD") || strings.Contains(strings.ToUpper(xepgChannel.XName), " 4K") {
video.Quality = "UHDTV"
}
}
program.Video = video
return
}
// Lokale Provider XMLTV Datei laden
func getLocalXMLTV(file string, xmltv *XMLTV) (err error) {
if _, ok := Data.Cache.XMLTV[file]; !ok {
// Cache initialisieren
if len(Data.Cache.XMLTV) == 0 {
Data.Cache.XMLTV = make(map[string]XMLTV)
}
// XML Daten lesen
content, err := readByteFromFile(file)
// Lokale XML Datei existiert nicht im Ordner: data
if err != nil {
ShowError(err, 1004)
err = errors.New("Local copy of the file no longer exists")
return err
}
// XML Datei parsen
err = xml.Unmarshal(content, &xmltv)
if err != nil {
return err
}
Data.Cache.XMLTV[file] = *xmltv
} else {
*xmltv = Data.Cache.XMLTV[file]
}
return
}
// M3U Datei erstellen
func createM3UFile() {
showInfo("XEPG:" + fmt.Sprintf("Create M3U file (%s)", System.File.M3U))
_, err := buildM3U([]string{})
if err != nil {
ShowError(err, 000)
}
saveMapToJSONFile(System.File.URLS, Data.Cache.StreamingURLS)
return
}
// XEPG Datenbank bereinigen
func cleanupXEPG() {
showInfo("XEPG:" + fmt.Sprintf("Cleanup database"))
Data.XEPG.XEPGCount = 0
for id, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
if err == nil {
if indexOfString(xepgChannel.Name, Data.Cache.Streams.Active) == -1 {
delete(Data.XEPG.Channels, id)
} else {
if xepgChannel.XActive == true {
Data.XEPG.XEPGCount++
}
}
}
}
err := saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
if err != nil {
ShowError(err, 000)
return
}
showInfo("XEPG Channels:" + fmt.Sprintf("%d", Data.XEPG.XEPGCount))
if len(Data.Streams.Active) > 0 && Data.XEPG.XEPGCount == 0 {
showWarning(2005)
}
return
}
// Streaming URL für die Channels App generieren
func getStreamByChannelID(channelID string) (playlistID, streamURL string, err error) {
err = errors.New("Channel not found")
for _, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
fmt.Println(xepgChannel.XChannelID)
if err == nil {
if channelID == xepgChannel.XChannelID {
playlistID = xepgChannel.FileM3UID
streamURL = xepgChannel.URL
return playlistID, streamURL, nil
}
}
}
return
}

47
ts/authentication_ts.ts Normal file
View File

@@ -0,0 +1,47 @@
function login() {
var err:Boolean = false
var data = new Object()
var div:any = document.getElementById("content")
var form:any = document.getElementById("authentication")
var inputs:any = div.getElementsByTagName("INPUT")
console.log(inputs)
for (var i = inputs.length - 1; i >= 0; i--) {
var key:string = (inputs[i] as HTMLInputElement).name
var value:string = (inputs[i] as HTMLInputElement).value
if (value.length == 0) {
inputs[i].style.borderColor = "red"
err = true
}
data[key] = value
}
if (err == true) {
data = new Object()
return
}
if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) {
alert("sdafsd")
document.getElementById('password').style.borderColor = "red"
document.getElementById('confirm').style.borderColor = "red"
document.getElementById("err").innerHTML = "{{.account.failed}}"
return
}
}
console.log(data)
form.submit();
}

663
ts/base_ts.ts Normal file
View File

@@ -0,0 +1,663 @@
var SERVER = new Object()
var BULK_EDIT:Boolean = false
var COLUMN_TO_SORT:number
var SEARCH_MAPPING = new Object()
var UNDO = new Object()
var SERVER_CONNECTION = false
var WS_AVAILABLE = false
// Menü
var menuItems = new Array()
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"))
//menuItems.push(new MainMenuItem("pmsID", "{{.mainMenu.item.pmsID}}", "number.png", "{{.mainMenu.headline.pmsID}}"))
menuItems.push(new MainMenuItem("filter", "{{.mainMenu.item.filter}}", "filter.png", "{{.mainMenu.headline.filter}}"))
menuItems.push(new MainMenuItem("xmltv", "{{.mainMenu.item.xmltv}}", "xmltv.png", "{{.mainMenu.headline.xmltv}}"))
menuItems.push(new MainMenuItem("mapping", "{{.mainMenu.item.mapping}}", "mapping.png", "{{.mainMenu.headline.mapping}}"))
menuItems.push(new MainMenuItem("users", "{{.mainMenu.item.users}}", "users.png", "{{.mainMenu.headline.users}}"))
menuItems.push(new MainMenuItem("settings", "{{.mainMenu.item.settings}}", "settings.png", "{{.mainMenu.headline.settings}}"))
menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.mainMenu.headline.log}}"))
menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"))
// Kategorien für die Einstellungen
var settingsCategory = new Array()
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.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:string
switch(type) {
case true: cssClass = "block"; break;
case false: cssClass = "none"; break;
}
document.getElementById(elmID).className = cssClass;
}
function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value;
document.getElementById(buttonID).setAttribute(attribute, value)
}
function getLocalData(dataType, id):object {
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():string[] {
var channels:string[] = 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] as HTMLElement).style.display != "none") {
if ((trs[i].firstChild.firstChild as HTMLInputElement).checked == true) {
channels.push(trs[i].id)
}
}
}
return channels
}
function selectAllChannels() {
var bulk:Boolean = false
var trs = document.getElementById("content_table").getElementsByTagName("TR")
if ((trs[0].firstChild.firstChild as HTMLInputElement).checked == true) {
bulk = true
}
for (var i = 1; i < trs.length; i++) {
if ((trs[i] as HTMLElement).style.display != "none") {
switch (bulk) {
case true:
(trs[i].firstChild.firstChild as HTMLInputElement).checked = true
break
case false:
(trs[i].firstChild.firstChild as HTMLInputElement).checked = false
break
}
}
}
return
}
function bulkEdit() {
BULK_EDIT = !BULK_EDIT
var className:string
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] as HTMLInputElement).checked = false
}
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 as HTMLTableElement).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 == "" || xValue == NaN) {
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:string[] = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title"]
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 {
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " "
}
})
})
return
}
function searchInMapping() {
var searchValue = (document.getElementById("searchMapping") as HTMLInputElement).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() {
if (document.getElementById("box-wrapper")){
var elm = document.getElementById("box-wrapper");
var divs = new Array("myStreamsBox", "clientInfo", "content");
var elementsHeight = 0 - elm.offsetHeight;
for (var i = 0; i < divs.length; i++) {
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
}
elm.style.height = window.innerHeight - elementsHeight + "px";
}
return
}
function changeChannelNumber(element) {
var dbID = element.parentNode.parentNode.id
var newNumber:number = parseFloat(element.value)
var channelNumbers:number[] = []
var data = SERVER["xepg"]["epgMapping"]
var channels = getObjKeys(data)
if (isNaN(newNumber)) {
alert("{{.alert.invalidChannelNumber}}")
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:Server = new Server(cmd)
server.request(data)
return
}
function toggleChannelStatus(id:string) {
var element:any
var status:boolean
if(document.getElementById("active")) {
var checkbox = (document.getElementById("active") as HTMLInputElement)
status = (checkbox).checked
}
var ids:string[] = 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) {
alert(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 as HTMLInputElement).files[0].name
var check = confirm("File: " + filename + "\n{{.confirm.restore}}");
if (check == true) {
var reader = new FileReader();
var file = (document.querySelector('input[type=file]') as HTMLInputElement).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:Server = new Server(cmd)
server.request(data)
};
} else {
alert("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 as HTMLInputElement).files[0].name
var reader = new FileReader();
var file = (document.querySelector('input[type=file]') as HTMLInputElement).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:Server = new Server(cmd)
server.request(data)
var updateLogo = (document.getElementById('update-icon') as HTMLInputElement)
updateLogo.checked = false
updateLogo.className = "changed"
};
} else {
alert("File could not be loaded")
}
upload.remove()
return
}
}
function checkUndo(key:string) {
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() {
console.log("TOKEN")
var server:Server = new Server("updateLog")
server.request(new Object())
}

3
ts/compileJS.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
tsc *.ts --outDir ../html/js/

169
ts/configuration_ts.ts Normal file
View File

@@ -0,0 +1,169 @@
class WizardCategory {
DocumentID = "content"
createCategoryHeadline(value:string):any {
var element = document.createElement("H4")
element.innerHTML = value
return element
}
}
class WizardItem extends WizardCategory {
key:string
headline:string
constructor(key:string, headline:string) {
super()
this.headline = headline
this.key = key
}
createWizard():void {
var headline = this.createCategoryHeadline(this.headline)
var key = this.key
var content:PopupContent = new PopupContent()
var description:string
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 = "{{.wizard.tuner.description}}"
break;
case "epgSource":
var text:any[] = ["PMS", "XEPG"]
var values:any[] = ["PMS", "XEPG"]
var select = content.createSelect(text, values, "XEPG", key)
select.setAttribute("class", "wizard")
select.id = key
doc.appendChild(select)
description = "{{.wizard.epgSource.description}}"
break
case "m3u":
var input = content.createInput("text", key, "")
input.setAttribute("class", "wizard")
input.id = key
doc.appendChild(input)
description = "{{.wizard.m3u.description}}"
break
case "xmltv":
var input = content.createInput("text", key, "")
input.setAttribute("class", "wizard")
input.id = key
doc.appendChild(input)
description = "{{.wizard.xmltv.description}}"
break
default:
console.log(key)
break;
}
var pre = document.createElement("PRE")
pre.innerHTML = description
doc.appendChild(pre)
console.log(headline, key)
}
}
function readyForConfiguration(wizard:number) {
var server: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:string
var value:any
switch (config[i].tagName) {
case "SELECT":
name = (config[i] as HTMLSelectElement).name
value = (config[i] as HTMLSelectElement).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] as HTMLInputElement).type) {
case "text":
name = (config[i] as HTMLInputElement).name
value = (config[i] as HTMLInputElement).value
wizard[name] = value
break
}
break
default:
// code...
break;
}
}
var data = new Object()
data["wizard"] = wizard
var server:Server = new Server(cmd)
server.request(data)
console.log(data)
}
// Wizard
var configurationWizard = new Array()
configurationWizard.push(new WizardItem("tuner", "{{.wizard.tuner.title}}"))
configurationWizard.push(new WizardItem("epgSource", "{{.wizard.epgSource.title}}"))
configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}"))
configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.title}}"))

65
ts/logs_ts.ts Normal file
View File

@@ -0,0 +1,65 @@
class Log {
createLog(entry:string):any {
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.innerHTML = entry
return element
}
}
function showLogs(bottom:boolean) {
var log = new Log()
var logs = SERVER["log"]["log"]
var div = document.getElementById("content_log")
div.innerHTML = ""
var keys = getObjKeys(logs)
keys.forEach(logID => {
var entry = log.createLog(logs[logID])
div.append(entry)
});
setTimeout(function(){
if (bottom == true) {
var wrapper = document.getElementById("box-wrapper");
wrapper.scrollTop = wrapper.scrollHeight;
}
}, 10);
}
function resetLogs() {
var cmd = "resetLogs"
var data = new Object()
var server:Server = new Server(cmd)
server.request(data)
}

2198
ts/menu_ts.ts Normal file

File diff suppressed because it is too large Load Diff

147
ts/network_ts.ts Normal file
View File

@@ -0,0 +1,147 @@
class Server {
protocol:string
cmd:string
constructor(cmd:string) {
this.cmd = cmd
}
request(data:Object):any {
if (SERVER_CONNECTION == true) {
return
}
SERVER_CONNECTION = true
console.log(data)
if (this.cmd != "updateLog") {
showElement("loading", true)
UNDO = new Object()
}
switch(window.location.protocol) {
case "http:":
this.protocol = "ws://"
break
case "https://":
this.protocol = "wss://"
break
}
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token")
data["cmd"] = this.cmd
var ws = new WebSocket(url)
ws.onopen = function() {
WS_AVAILABLE = true
console.log("REQUEST (JS):");
console.log(data)
console.log("REQUEST: (JSON)");
console.log(JSON.stringify(data))
this.send(JSON.stringify(data));
}
ws.onerror = function(e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.")
SERVER_CONNECTION = false
if (WS_AVAILABLE == false) {
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
}
}
ws.onmessage = function (e) {
SERVER_CONNECTION = false
showElement("loading", false)
console.log("RESPONSE:");
var response = JSON.parse(e.data);
console.log(response);
if (response.hasOwnProperty("token")) {
document.cookie = "Token=" + response["token"]
}
if (response["status"] == false) {
alert(response["err"])
if (response.hasOwnProperty("reload")) {
location.reload()
}
return
}
if (response.hasOwnProperty("logoURL")) {
var div = (document.getElementById("channel-icon") as HTMLInputElement)
div.value = response["logoURL"]
div.className = "changed"
return
}
switch (data["cmd"]) {
case "updateLog":
SERVER["log"] = response["log"]
if (document.getElementById("content_log")) {
showLogs(false)
}
return
break;
default:
SERVER = new Object()
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")) {
alert(response["alert"])
}
if (response.hasOwnProperty("reload")) {
location.reload()
}
if (response.hasOwnProperty("wizard")) {
createLayout()
configurationWizard[response["wizard"]].createWizard()
return
}
createLayout()
}
}
}
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}

564
ts/settings_ts.ts Normal file
View File

@@ -0,0 +1,564 @@
class SettingsCategory {
DocumentID:string = "content_settings"
createCategoryHeadline(value:string):any {
var element = document.createElement("H4")
element.innerHTML = value
return element
}
createHR():any {
var element = document.createElement("HR")
return element
}
createSettings(settingsKey:string):any {
var setting = document.createElement("TR")
var content:PopupContent = new PopupContent()
var data = SERVER["settings"][settingsKey]
switch (settingsKey) {
// Texteingaben
case "update":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.update.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "update", data.toString())
input.setAttribute("placeholder", "{{.settings.update.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "backup.path":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.backupPath.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "backup.path", data)
input.setAttribute("placeholder", "{{.settings.backupPath.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "temp.path":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tempPath.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "temp.path", data)
input.setAttribute("placeholder", "{{.settings.tmpPath.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "user.agent":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.userAgent.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "user.agent", data)
input.setAttribute("placeholder", "{{.settings.userAgent.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "buffer.timeout":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "buffer.timeout", data)
input.setAttribute("placeholder", "{{.settings.bufferTimeout.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
// Checkboxen
case "authentication.web":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationWEB.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "authentication.pms":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationPMS.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "authentication.m3u":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationM3U.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "authentication.xml":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationXML.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "authentication.api":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.authenticationAPI.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "files.update":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.filesUpdate.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "cache.images":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.cacheImages.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "xepg.replace.missing.images":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.replaceEmptyImages.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "xteveAutoUpdate":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.xteveAutoUpdate.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "buffer":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "api":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.api.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
// Select
case "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
var tdRight = document.createElement("TD")
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, data, settingsKey)
select.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(select)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "epgSource":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.epgSource.title}}" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["PMS", "XEPG"]
var values:any[] = ["PMS", "XEPG"]
var select = content.createSelect(text, values, data, settingsKey)
select.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(select)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "backup.keep":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.backupKeep.title}}" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["5", "10", "20", "30", "40", "50"]
var values:any[] = ["5", "10", "20", "30", "40", "50"]
var select = content.createSelect(text, values, data, settingsKey)
select.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(select)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "buffer.size.kb":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.bufferSize.title}}" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["0.5 MB", "1 MB", "2 MB", "3 MB", "4 MB", "5 MB", "6 MB", "7 MB", "8 MB"]
var values:any[] = ["512", "1024", "2048", "3072", "4096", "5120", "6144", "7168", "8192"]
var select = content.createSelect(text, values, data, settingsKey)
select.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(select)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
}
return setting
}
createDescription(settingsKey:string):any {
var description = document.createElement("TR")
var text:string
switch (settingsKey) {
case "authentication.web":
text = "{{.settings.authenticationWEB.description}}"
break
case "authentication.m3u":
text = "{{.settings.authenticationM3U.description}}"
break
case "authentication.pms":
text = "{{.settings.authenticationPMS.description}}"
break
case "authentication.xml":
text = "{{.settings.authenticationXML.description}}"
break
case "authentication.api":
if (SERVER["settings"]["authentication.web"] == true) {
text = "{{.settings.authenticationAPI.description}}"
}
break
case "xteveAutoUpdate":
text = "{{.settings.xteveAutoUpdate.description}}"
break
case "backup.keep":
text = "{{.settings.backupKeep.description}}"
break
case "backup.path":
text = "{{.settings.backupPath.description}}"
break
case "temp.path":
text = "{{.settings.tempPath.description}}"
break
case "buffer":
text = "{{.settings.streamBuffering.description}}"
break
case "buffer.size.kb":
text = "{{.settings.bufferSize.description}}"
break
case "buffer.timeout":
text = "{{.settings.bufferTimeout.description}}"
break
case "user.agent":
text = "{{.settings.userAgent.description}}"
break
case "epgSource":
text = "{{.settings.epgSource.description}}"
break
case "tuner":
text = "{{.settings.tuner.description}}"
break
case "update":
text = "{{.settings.update.description}}"
break
case "api":
text = "{{.settings.api.description}}"
break
case "files.update":
text = "{{.settings.filesUpdate.description}}"
break
case "cache.images":
text = "{{.settings.cacheImages.description}}"
break
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"
break
default:
text = ""
break
}
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 {
headline:string
settingsKeys:string
constructor(headline:string, settingsKeys:string) {
super()
this.headline = headline
this.settingsKeys = settingsKeys
}
createCategory():void {
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:string
var value:any
switch (settings[i].tagName) {
case "INPUT":
switch ((settings[i] as HTMLInputElement).type) {
case "checkbox":
name = (settings[i] as HTMLInputElement).name
value = (settings[i] as HTMLInputElement).checked
newSettings[name] = value
break
case "text":
name = (settings[i] as HTMLInputElement).name
value = (settings[i] as HTMLInputElement).value
switch (name) {
case "update":
value = value.split(",")
value = value.filter(function(e:any) { return e})
break
case "buffer.timeout":
value = parseFloat(value)
}
newSettings[name] = value
break
}
break
case "SELECT":
name = (settings[i] as HTMLSelectElement).name
value = (settings[i] as HTMLSelectElement).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:Server = new Server(cmd)
server.request(data)
}

171
xteve.go Normal file
View File

@@ -0,0 +1,171 @@
// Copyright 2019 marmei. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the
// LICENSE file.
// GitHub: https://github.com/xteve-project/xTeVe
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"./src"
)
// GitHubStruct : GitHub Account. Über diesen Account werden die Updates veröffentlicht
type GitHubStruct struct {
Branch string
Repo string
Update bool
User string
}
// GitHub : GitHub Account
// If you want to fork this project, enter your Github account here. This prevents a newer version of xTeVe from updating your version.
var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-Downloads", Update: true}
/*
Branch: GitHub Branch
User: GitHub Username
Repo: GitHub Repository
Update: Automatic updates from the GitHub repository [true|false]
*/
// Name : Programname
const Name = "xTeVe"
// Version : Version, die Build Nummer wird in der main func geparst.
const Version = "2.0.0.0000"
// APIVersion : API Version
const APIVersion = "1.1.0"
// Dev : Aktiviert den Entwicklungsmodus. Für den Webserver werden dann die lokalen Dateien verwendet.
const Dev = false
var homeDirectory = fmt.Sprintf("%s%s.%s%s", src.GetUserHomeDirectory(), string(os.PathSeparator), strings.ToLower(Name), string(os.PathSeparator))
var samplePath = fmt.Sprintf("%spath%sto%sxteve%s", string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator))
var configFolder = flag.String("config", "", ": Config Folder ["+samplePath+"] (default: "+homeDirectory+")")
var port = flag.String("port", "", ": Server port [34400] (default: 34400)")
var gitBranch = flag.String("branch", "", ": Git Branch [master|beta] (default: master)")
var debug = flag.Int("debug", 0, ": Debug level [0 - 3] (default: 0)")
var h = flag.Bool("h", false, ": Show help")
func main() {
// Build-Nummer von der Versionsnummer trennen
var build = strings.Split(Version, ".")
var system = &src.System
system.APIVersion = APIVersion
system.Branch = GitHub.Branch
system.Build = build[len(build)-1:][0]
system.Dev = Dev
system.GitHub = GitHub
system.Name = Name
system.Version = strings.Join(build[0:len(build)-1], ".")
// Panic !!!
defer func() {
if r := recover(); r != nil {
fmt.Println()
fmt.Println("* * * * * FATAL ERROR * * * * *")
fmt.Println("OS: ", runtime.GOOS)
fmt.Println("Arch:", runtime.GOARCH)
fmt.Println("Err: ", r)
fmt.Println()
pc := make([]uintptr, 20)
runtime.Callers(2, pc)
for i := range pc {
if runtime.FuncForPC(pc[i]) != nil {
f := runtime.FuncForPC(pc[i])
file, line := f.FileLine(pc[i])
if string(file)[0:1] != "?" {
fmt.Printf("%s:%d %s\n", filepath.Base(file), line, f.Name())
}
}
}
fmt.Println()
fmt.Println("* * * * * * * * * * * * * * * *")
}
}()
flag.Parse()
if *h {
flag.Usage()
return
}
// Webserver Port
if len(*port) > 0 {
system.Flag.Port = *port
}
// Branch
system.Flag.Branch = *gitBranch
if len(system.Flag.Branch) > 0 {
fmt.Println("Git Branch is now:", system.Flag.Branch)
}
// Debug Level
system.Flag.Debug = *debug
if system.Flag.Debug > 3 {
flag.Usage()
return
}
// Speicherort für die Konfigurationsdateien
if len(*configFolder) > 0 {
system.Folder.Config = *configFolder
}
err := src.Init()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
err = src.BinaryUpdate()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
err = src.StartSystem(false)
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
err = src.InitMaintenance()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
err = src.StartWebserver()
if err != nil {
src.ShowError(err, 0)
os.Exit(0)
}
}