27 Commits

Author SHA1 Message Date
f558a855ae go fix
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-18 15:12:33 +11:00
125b0bb35f Enhance XEPG channel mapping and settings management
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-13 16:09:00 +11:00
32c3d779c0 add edit button to mapping table and refactor cell creation
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-12 13:22:51 +11:00
ce5b12d8b8 Enhance Docker configuration with UID and GID arguments for improved user permissions
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 19:49:23 +11:00
76183bfaa2 Refactor entrypoint script for improved configuration handling and migration logic
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 19:10:07 +11:00
c577d354e7 Enhance configuration handling and implement wizard completion logic
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 17:15:54 +11:00
e48a061ca0 Enhance WebSocket handling and log polling logic
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 16:25:48 +11:00
ffd43d5217 Enhance log display behavior and menu state management
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 14:37:02 +11:00
9bd2b32003 Enhance WebSocket connection handling with improved timeout and error states
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 14:20:24 +11:00
8698983bbf updates
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 13:28:59 +11:00
60af423335 update UI js
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 13:09:41 +11:00
57b6be74e2 improve ui checkbox functionality
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 13:03:12 +11:00
c5545cbf08 go fix for 1.26
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 12:53:35 +11:00
339d2d0aa5 include ffmpeg
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 12:42:01 +11:00
a04b0ede50 fix tmp dir permissions crash
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 12:29:49 +11:00
84de46a2f2 cleanup version numbers
All checks were successful
continuous-integration/drone/push Build is passing
2026-02-11 12:20:36 +11:00
5eaf5efdb6 improve Dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 12:13:10 +11:00
fb62353de4 fix docker image location
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 12:08:58 +11:00
fa9d41e10c and again
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-02-11 12:07:17 +11:00
e07d4dd878 again
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 12:01:14 +11:00
6053207034 pipeline fix
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 11:58:47 +11:00
6a8b4bed28 try again
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 11:56:30 +11:00
45685ce592 fix drone yml
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 11:53:44 +11:00
43a9cf5a7e bugfix
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-02-11 11:52:34 +11:00
a23cc7a183 change pipeline
Some checks failed
continuous-integration/drone/push Build is failing
2026-02-11 11:40:30 +11:00
b069d5bee8 many updates 2026-02-11 11:38:26 +11:00
8cb9e43a72 Redesign UI and add first-party Docker runtime support 2026-02-11 11:04:39 +11:00
57 changed files with 4894 additions and 1701 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.github
.DS_Store
docker-data
README-DEV.md
*.log
*.tmp

63
.drone.yml Normal file
View File

@@ -0,0 +1,63 @@
kind: pipeline
type: docker
name: default
steps:
- name: go-test
image: cache.coadcorp.com/library/golang
commands:
- go mod download
- go test ./...
- name: go-build
image: cache.coadcorp.com/library/golang
commands:
- XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed 's/-beta$//')"
- test -n "$XTEVE_VERSION" || (echo "Could not parse version from changelog-beta.md" && exit 1)
- echo "Building xTeVe version $XTEVE_VERSION from changelog-beta.md"
- go build -v -ldflags "-X main.Version=$XTEVE_VERSION" ./...
- name: dockerfile-lint
image: cache.coadcorp.com/hadolint/hadolint:v2.12.0-alpine
commands:
- hadolint -t error Dockerfile
- name: compose-validate
image: cache.coadcorp.com/library/docker:27-cli
commands:
- apk add --no-cache docker-cli-compose
- docker compose -f docker-compose.yml config -q
- docker compose -f docker-compose.host.yml config -q
- name: docker-build-validate
image: gcr.io/kaniko-project/executor:v1.23.2-debug
commands:
- /kaniko/executor --context "${DRONE_WORKSPACE}" --dockerfile "${DRONE_WORKSPACE}/Dockerfile" --no-push --destination xteve:validate --build-arg TARGETOS=linux --build-arg TARGETARCH=amd64 --build-arg XTEVE_UID=1000 --build-arg XTEVE_GID=1000
when:
event:
- pull_request
- name: docker-publish
image: plugins/docker
environment:
XTEVE_UID: "1000"
XTEVE_GID: "1000"
settings:
registry: registry.coadcorp.com
repo: registry.coadcorp.com/nathan/xteve
dockerfile: Dockerfile
username: nathan
password:
from_secret: registry_password
tags:
- latest
- ${DRONE_COMMIT_SHA}
build_args:
- TARGETOS=linux
- TARGETARCH=amd64
build_args_from_env:
- XTEVE_UID
- XTEVE_GID
when:
event:
- push

52
Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
# syntax=docker/dockerfile:1.7
FROM golang:1.26-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates tzdata
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go run ./cmd/webui-gen
ARG TARGETOS
ARG TARGETARCH
RUN XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed 's/-beta$//')" \
&& test -n "$XTEVE_VERSION" \
&& CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -trimpath -ldflags="-s -w -X main.Version=$XTEVE_VERSION" -o /out/xteve ./xteve.go
FROM mwader/static-ffmpeg:latest AS ffmpeg
FROM alpine:3.23
ARG XTEVE_UID=1000
ARG XTEVE_GID=1000
RUN apk add --no-cache ca-certificates tzdata \
&& addgroup -S -g "${XTEVE_GID}" xteve \
&& adduser -S -D -H -u "${XTEVE_UID}" -G xteve xteve \
&& mkdir -p /xteve/config \
&& chown -R xteve:xteve /xteve
WORKDIR /xteve
COPY --from=builder /out/xteve /usr/local/bin/xteve
COPY --from=ffmpeg /ffmpeg /usr/local/bin/ffmpeg
COPY --from=ffmpeg /ffprobe /usr/local/bin/ffprobe
COPY docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
USER xteve
EXPOSE 34400/tcp
ENV XTEVE_CONFIG=/xteve/config
ENV XTEVE_PORT=34400
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- "http://127.0.0.1:${XTEVE_PORT}/lineup_status.json" > /dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

View File

@@ -48,6 +48,59 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
---
## Project Analysis (UI + Operations)
The core architecture is strong: a Go backend with websocket-driven UI updates, filesystem-based state, and very low runtime overhead.
The weakest points are mostly operational and UX-focused:
* UI was historically utility-first and desktop-biased, with limited responsive behavior and visual hierarchy.
* Container usage was documented externally but there was no first-party Dockerfile/compose setup in this repository.
* Static web assets are generated into `src/webUI.go`, which works, but creates large diffs and a heavier edit/build cycle.
### Recommended next technical improvements
1. Replace generated `src/webUI.go` with Go `embed` for simpler static asset management and cleaner PR diffs.
2. Add CI checks (`go test ./...`, build on Linux/arm64/amd64, docker build smoke test).
3. Add a dedicated health endpoint (for example `/healthz`) to decouple health checks from HDHomeRun endpoints.
4. Add integration tests around websocket commands that mutate settings/files to reduce regression risk.
---
## Container-First Run (Included In This Repo)
### Build image
```bash
docker build -t xteve:local .
```
### Run with Docker Compose (bridge mode)
```bash
docker compose up -d
```
Compose file: `docker-compose.yml`
Persistent config volume: `./docker-data/config:/xteve/config`
### Run with Docker Compose (host networking, Linux recommended for discovery)
```bash
docker compose -f docker-compose.host.yml up -d
```
Host networking improves LAN discovery behavior (SSDP/DLNA) for Plex/Emby in many setups.
### Container environment variables
* `XTEVE_CONFIG` (default: `/xteve/config`)
* `XTEVE_PORT` (default: `34400`)
### Image details
* Multi-stage build (Go builder + minimal Alpine runtime)
* Runs as non-root user (`xteve`)
* Built-in healthcheck against `http://127.0.0.1:${XTEVE_PORT}/lineup_status.json`
---
## Downloads v2 | 64 Bit only
#### 64 Bit Intel / AMD
@@ -156,4 +209,3 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
```

View File

@@ -1,3 +1,19 @@
#### 2.2.0.0201-beta
Add UI enhancement for range selecting checkboxes
#### 2.2.0.0200-beta
```diff
+ Major web UI redesign focused on cleaner layout, better information hierarchy, and faster daily workflows.
+ Large mobile UX overhaul: responsive navigation, improved spacing, touch-friendly controls, and better small-screen mapping/config flows.
+ Accessibility pass across auth, configuration, and main app screens (focus visibility, keyboard flow, ARIA/announcer updates, contrast and status feedback improvements).
+ Settings UX improvements and additional polish in frontend behavior for menu, configuration, and authentication interactions.
+ Added optional Plex API refresh integration with new settings: use_plexAPI, plex.url, plex.token.
+ Added debounced/queued Plex DVR guide reload workflow after lineup and XEPG updates to reduce manual refresh work in Plex.
+ Container/runtime improvements and validation updates for easier, safer container usage.
+ Added Drone CI pipeline for tests/build checks, Docker/Compose validation, and Docker image publishing to registry.coadcorp.com via plugins/docker.
+ Pipeline build version is now derived from changelog-beta.md and validated against source version to prevent release/version drift.
```
#### 2.1.1.0116-beta
If no user agent is specified, the default FFmpeg or VLC user agent is used.

15
cmd/webui-gen/main.go Normal file
View File

@@ -0,0 +1,15 @@
package main
import (
"log"
"os"
"xteve/src"
)
func main() {
src.HTMLInit("webUI", "src", "html"+string(os.PathSeparator), "src"+string(os.PathSeparator)+"webUI.go")
if err := src.BuildGoFile(); err != nil {
log.Fatal(err)
}
}

18
docker-compose.host.yml Normal file
View File

@@ -0,0 +1,18 @@
services:
xteve:
build:
context: .
dockerfile: Dockerfile
args:
XTEVE_UID: ${XTEVE_UID:-1000}
XTEVE_GID: ${XTEVE_GID:-1000}
container_name: xteve
restart: unless-stopped
network_mode: host
environment:
XTEVE_CONFIG: /xteve/config
XTEVE_PORT: "34400"
XTEVE_UID: ${XTEVE_UID:-1000}
XTEVE_GID: ${XTEVE_GID:-1000}
volumes:
- ./docker-data/config:/xteve/config

20
docker-compose.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
xteve:
build:
context: .
dockerfile: Dockerfile
args:
XTEVE_UID: ${XTEVE_UID:-1000}
XTEVE_GID: ${XTEVE_GID:-1000}
container_name: xteve
restart: unless-stopped
environment:
XTEVE_CONFIG: /xteve/config
XTEVE_PORT: "34400"
XTEVE_UID: ${XTEVE_UID:-1000}
XTEVE_GID: ${XTEVE_GID:-1000}
ports:
- "34400:34400/tcp"
- "1900:1900/udp"
volumes:
- ./docker-data/config:/xteve/config

97
docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/bin/sh
set -eu
DEFAULT_CONFIG_DIR="/xteve/config"
LEGACY_CONFIG_DIRS="/config /xteve /home/xteve/.xteve"
resolve_config_dir() {
if [ -n "${XTEVE_CONFIG:-}" ] && [ "${XTEVE_CONFIG}" != "${DEFAULT_CONFIG_DIR}" ]; then
printf "%s" "${XTEVE_CONFIG}"
return
fi
if [ -f "${DEFAULT_CONFIG_DIR}/settings.json" ]; then
printf "%s" "${DEFAULT_CONFIG_DIR}"
return
fi
for dir in ${LEGACY_CONFIG_DIRS}; do
if [ -f "${dir}/settings.json" ]; then
printf "%s" "${dir}"
return
fi
done
printf "%s" "${DEFAULT_CONFIG_DIR}"
}
copy_if_missing() {
src="$1"
dst="$2"
if [ -e "${src}" ] && [ ! -e "${dst}" ]; then
cp -R "${src}" "${dst}"
fi
}
settings_initialized() {
file="$1"
if [ ! -s "${file}" ]; then
return 1
fi
if grep -q '"uuid"' "${file}" 2>/dev/null; then
return 0
fi
return 1
}
CONFIG_DIR="$(resolve_config_dir)"
PORT="${XTEVE_PORT:-34400}"
mkdir -p "${CONFIG_DIR}"
if ! settings_initialized "${CONFIG_DIR}/settings.json"; then
for legacy_dir in ${LEGACY_CONFIG_DIRS}; do
if [ "${legacy_dir}" = "${CONFIG_DIR}" ]; then
continue
fi
if settings_initialized "${legacy_dir}/settings.json"; then
echo "[entrypoint] Migrating existing configuration from ${legacy_dir} to ${CONFIG_DIR}"
for file in authentication.json pms.json settings.json xepg.json urls.json; do
if [ "${file}" = "settings.json" ] || [ "${file}" = "xepg.json" ] || [ "${file}" = "urls.json" ]; then
cp -f "${legacy_dir}/${file}" "${CONFIG_DIR}/${file}" 2>/dev/null || true
else
copy_if_missing "${legacy_dir}/${file}" "${CONFIG_DIR}/${file}"
fi
done
for dir_name in data cache backup tmp; do
copy_if_missing "${legacy_dir}/${dir_name}" "${CONFIG_DIR}/${dir_name}"
done
break
fi
done
fi
if ! touch "${CONFIG_DIR}/.xteve-write-test" 2>/dev/null; then
echo "[entrypoint] ERROR: Config directory is not writable: ${CONFIG_DIR}" >&2
echo "[entrypoint] Running as UID:GID $(id -u):$(id -g)" >&2
ls -ld "${CONFIG_DIR}" >&2 || true
echo "[entrypoint] Hint: ensure host path ownership/permissions allow this UID:GID to write, or set matching container UID/GID at build time." >&2
exit 1
fi
rm -f "${CONFIG_DIR}/.xteve-write-test"
echo "[entrypoint] Using config directory: ${CONFIG_DIR}"
echo "[entrypoint] Running as UID:GID $(id -u):$(id -g)"
if [ -f "${CONFIG_DIR}/settings.json" ]; then
echo "[entrypoint] settings.json details: $(ls -l "${CONFIG_DIR}/settings.json" | awk '{print $1, $3, $4, $5, $9}')"
fi
exec /usr/local/bin/xteve -config "${CONFIG_DIR}" -port "${PORT}" "$@"

13
go.mod
View File

@@ -1,9 +1,14 @@
module xteve
go 1.16
go 1.25
require (
github.com/gorilla/websocket v1.4.2 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/koron/go-ssdp v0.0.2 // indirect
github.com/gorilla/websocket v1.5.3
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/koron/go-ssdp v0.1.0
)
require (
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
)

12
go.sum
View File

@@ -1,16 +1,28 @@
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/koron/go-ssdp v0.0.2 h1:fL3wAoyT6hXHQlORyXUW4Q23kkQpJRgEAYcZB5BR71o=
github.com/koron/go-ssdp v0.0.2/go.mod h1:XoLfkAiA2KeZsYh4DbHxD7h3nR2AZNqVQOa+LJuqPYs=
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -13,16 +13,16 @@
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
</head>
<body onload="javascript: readyForConfiguration(0);">
<body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);">
<div id="loading" class="block">
<div id="loading" class="block" role="status" aria-live="polite" aria-label="Loading" aria-hidden="false">
<div class="loader"></div>
</div>
<div id="header" class="imgCenter"></div>
<div id="box">
<main id="box" role="main" aria-labelledby="head-text">
<table id="clientInfo" class="visible">
<table id="clientInfo" class="visible" aria-label="Server information">
<tr>
<td class="tdKey">Version:</td>
<td id="version" class="tdVal">&nbsp;</td>
@@ -46,13 +46,14 @@
<div id="headline">
<h1 id="head-text" class="center">Configuration</h1>
</div>
<p id="err" class="errorMsg center"></p>
<div id="content">
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
<div id="content" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Configuration step">
</div>
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
<div id="box-footer">
<input id="next" class="" type="button" name="next" value="Next" onclick="javascript: saveWizard();">
<input id="next" class="" type="button" name="next" value="Next" aria-controls="content" onclick="javascript: saveWizard();">
</div>
</div>
</main>
</body>
</html>

View File

@@ -10,38 +10,38 @@
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
<div id="box">
<main id="box" role="main" aria-labelledby="head-text">
<div id="headline">
<h1 id="head-text" class="center">{{.account.headline}}</h1>
</div>
<p id="err" class="errorMsg center"></p>
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
<div id="content">
<form id="authentication" action="" method="post">
<form id="authentication" action="" method="post" aria-describedby="err" novalidate>
<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="">
<label for="username">{{.account.username.title}}:</label>
<input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
<label for="password">{{.account.password.title}}:</label>
<input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="new-password">
<label for="confirm">{{.account.confirm.title}}:</label>
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value="" autocomplete="new-password">
</form>
</div>
<div id="box-footer">
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" onclick="javascript: login();">
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" aria-label="{{.button.craeteAccount}}" onclick="javascript: login();">
</div>
</div>
</main>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,7 @@
<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">
@@ -17,16 +15,19 @@
</head>
<body onload="javascript: PageReady();">
<body class="app-shell" onload="javascript: PageReady();">
<div id="loading" class="none">
<a class="skip-link" href="#content">Skip to main content</a>
<div id="loading" class="none" role="status" aria-live="polite" aria-label="Loading" aria-hidden="true">
<div class="loader"></div>
</div>
<div id="popup" class="none">
<div id="popup-custom"></div>
<div id="popup" class="none" role="dialog" aria-modal="true" aria-hidden="true" tabindex="-1">
<div id="popup-custom" role="document" tabindex="-1"></div>
</div>
<div id="layout-overlay" aria-hidden="true" tabindex="-1"></div>
<div id="layout">
<!--
@@ -40,55 +41,62 @@
</div>
-->
<div id="menu-wrapper" class="layout-left">
<aside id="menu-wrapper" class="layout-left" aria-label="Sidebar menu">
<div id= "branch"></div>
<div id="logo"></div>
<nav id="main-menu"></nav>
</div>
<nav id="main-menu" role="menubar" aria-label="Main navigation"></nav>
</aside>
<div class="layout-right">
<main id="shell-main" class="layout-right">
<header id="shell-header">
<button id="menu-toggle" type="button" aria-expanded="false" aria-controls="menu-wrapper" aria-label="Toggle navigation menu">Menu</button>
<h2 id="shell-title">xTeVe Control Panel</h2>
<p id="connection-indicator" class="status-idle" role="status" aria-live="polite" aria-atomic="true">Connecting...</p>
</header>
<table id="clientInfo" class="">
<table id="clientInfo" class="" aria-label="Server information">
<tr>
<td class="tdKey">xTeVe:</td>
<td id="version" class="tdVal">&nbsp;</td>
<td id="version" class="tdVal" data-label="xTeVe">&nbsp;</td>
<td class="tdKey">OS:</td>
<td id="os" class="tdVal">&nbsp;</td>
<td id="os" class="tdVal" data-label="OS">&nbsp;</td>
<td class="tdKey phone">DVR IP:</td>
<td id="DVR" class="tdVal phone">&nbsp;</td>
<td id="DVR" class="tdVal phone" data-label="DVR IP">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">UUID:</td>
<td id="uuid" class="tdVal">&nbsp;</td>
<td id="uuid" class="tdVal" data-label="UUID">&nbsp;</td>
<td class="tdKey">Arch:</td>
<td id="arch" class="tdVal">&nbsp;</td>
<td id="arch" class="tdVal" data-label="Arch">&nbsp;</td>
<td class="tdKey phone">M3U URL:</td>
<td id="m3u-url" class="tdVal phone">&nbsp;</td>
<td id="m3u-url" class="tdVal phone" data-label="M3U URL">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">Available Streams:</td>
<td id="streams" class="tdVal">&nbsp;</td>
<td id="streams" class="tdVal" data-label="Available Streams">&nbsp;</td>
<td class="tdKey">EPG Source:</td>
<td id="epgSource" class="tdVal">&nbsp;</td>
<td id="epgSource" class="tdVal" data-label="EPG Source">&nbsp;</td>
<td class="tdKey phone">XEPG URL:</td>
<td id="xepg-url" class="tdVal phone">&nbsp;</td>
<td id="xepg-url" class="tdVal phone" data-label="XEPG URL">&nbsp;</td>
</tr>
<tr>
<td class="tdKey">XEPG Channels:</td>
<td id="xepg" class="tdVal">&nbsp;</td>
<td id="xepg" class="tdVal" data-label="XEPG Channels">&nbsp;</td>
<td class="tdKey">Errors:</td>
<td id="errors" class="tdVal">&nbsp;</td>
<td id="errors" class="tdVal" data-label="Errors">&nbsp;</td>
<td class="tdKey">Warnings:</td>
<td id="warnings" class="tdVal">&nbsp;</td>
<td id="warnings" class="tdVal" data-label="Warnings">&nbsp;</td>
</tr>
</table>
<div id="myStreamsBox" class="notVisible">
<div id="status-cards" class="dashboard-cards" role="list" aria-live="polite" aria-label="System summary"></div>
<div id="myStreamsBox" class="notVisible" aria-live="polite" aria-label="Stream details">
<div id="allStreams">
<table id="activeStreams"></table>
@@ -97,9 +105,10 @@
</div>
<div id="content" class=""></div>
<div id="content" class="" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Main content"></div>
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
</div>
</main>
</div>

View File

@@ -1,8 +1,13 @@
function login() {
var err = false;
var firstInvalid = null;
var data = new Object();
var div = document.getElementById("content");
var form = document.getElementById("authentication");
var errElement = document.getElementById("err");
if (errElement != null) {
errElement.innerHTML = "";
}
var inputs = div.getElementsByTagName("INPUT");
console.log(inputs);
for (var i = inputs.length - 1; i >= 0; i--) {
@@ -10,23 +15,54 @@ function login() {
var value = inputs[i].value;
if (value.length == 0) {
inputs[i].style.borderColor = "red";
inputs[i].setAttribute("aria-invalid", "true");
if (firstInvalid == null) {
firstInvalid = inputs[i];
}
err = true;
}
else {
inputs[i].style.borderColor = "";
inputs[i].setAttribute("aria-invalid", "false");
}
data[key] = value;
}
if (err == true) {
if (firstInvalid != null) {
firstInvalid.focus();
}
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('password').setAttribute("aria-invalid", "true");
document.getElementById('confirm').setAttribute("aria-invalid", "true");
document.getElementById("err").innerHTML = "{{.account.failed}}";
document.getElementById('password').focus();
return;
}
}
console.log(data);
form.submit();
}
document.addEventListener("DOMContentLoaded", function () {
var form = document.getElementById("authentication");
if (form == null) {
return;
}
var inputs = form.getElementsByTagName("INPUT");
for (var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener("keydown", function (event) {
if (event.key == "Enter") {
event.preventDefault();
login();
}
});
}
if (inputs.length > 0) {
inputs[0].focus();
}
});

View File

@@ -5,7 +5,10 @@ var SEARCH_MAPPING = new Object();
var UNDO = new Object();
var SERVER_CONNECTION = false;
var WS_AVAILABLE = false;
// Menü
var ACTIVE_MENU_ID = "";
var LAST_FOCUSED_ELEMENT = null;
var LAST_BULK_CHECKBOX = null;
// Menu
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}}"))
@@ -18,8 +21,8 @@ menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.
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.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
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"));
@@ -44,7 +47,74 @@ function showElement(elmID, type) {
cssClass = "none";
break;
}
document.getElementById(elmID).className = cssClass;
var element = document.getElementById(elmID);
if (element == null) {
return;
}
element.className = cssClass;
element.setAttribute("aria-hidden", type == true ? "false" : "true");
if (elmID == "loading" && document.body != null) {
document.body.setAttribute("aria-busy", type == true ? "true" : "false");
}
if (elmID == "popup") {
var popupContent_1 = document.getElementById("popup-custom");
if (type == true) {
LAST_FOCUSED_ELEMENT = document.activeElement;
if (popupContent_1 != null) {
setTimeout(function () {
popupContent_1.focus();
}, 20);
}
}
else {
if (LAST_FOCUSED_ELEMENT != null && LAST_FOCUSED_ELEMENT.focus != undefined) {
setTimeout(function () {
LAST_FOCUSED_ELEMENT.focus();
}, 20);
}
LAST_FOCUSED_ELEMENT = null;
}
}
}
function announceToScreenReader(message) {
if (message == undefined || message.length == 0) {
return;
}
var region = document.getElementById("sr-announcer");
if (region == null) {
return;
}
region.innerText = "";
setTimeout(function () {
region.innerText = message;
}, 20);
}
function setConnectionState(state, text) {
var label = text;
if (label == undefined || label.length == 0) {
switch (state) {
case "online":
label = "Connected";
break;
case "busy":
label = "Syncing";
break;
case "offline":
label = "Offline";
break;
default:
label = "Connecting";
break;
}
}
var indicator = document.getElementById("connection-indicator");
if (indicator == null) {
return;
}
indicator.className = "status-" + state;
indicator.innerText = label;
indicator.setAttribute("aria-label", "Connection status: " + label);
announceToScreenReader("Connection status " + label);
}
function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value;
@@ -114,6 +184,48 @@ function getAllSelectedChannels() {
}
return channels;
}
function scheduleChannelRangeSelection(checkbox, event) {
var shiftPressed = false;
if (event != undefined && event.shiftKey == true) {
shiftPressed = true;
}
// Run after the native checkbox toggle so we copy the final checked state.
setTimeout(function () {
selectChannelRange(checkbox, shiftPressed);
}, 0);
}
function selectChannelRange(checkbox, shiftPressed) {
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
return;
}
var table = document.getElementById("content_table");
if (table == null) {
return;
}
var trs = table.getElementsByTagName("TR");
var visibleCheckboxes = new Array();
for (var i = 1; i < trs.length; i++) {
if (trs[i].style.display != "none") {
var bulkCheckbox = trs[i].querySelector("input.bulk");
if (bulkCheckbox != null) {
visibleCheckboxes.push(bulkCheckbox);
}
}
}
var currentIndex = visibleCheckboxes.indexOf(checkbox);
var previousIndex = -1;
if (LAST_BULK_CHECKBOX != null) {
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX);
}
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
var start = Math.min(previousIndex, currentIndex);
var end = Math.max(previousIndex, currentIndex);
for (var i = start; i <= end; i++) {
visibleCheckboxes[i].checked = checkbox.checked;
}
}
LAST_BULK_CHECKBOX = checkbox;
}
function selectAllChannels() {
var bulk = false;
var trs = document.getElementById("content_table").getElementsByTagName("TR");
@@ -132,6 +244,7 @@ function selectAllChannels() {
}
}
}
LAST_BULK_CHECKBOX = null;
return;
}
function bulkEdit() {
@@ -150,6 +263,7 @@ function bulkEdit() {
rows[i].className = className;
rows[i].checked = false;
}
LAST_BULK_CHECKBOX = null;
return;
}
function sortTable(column) {
@@ -160,15 +274,29 @@ function sortTable(column) {
var table = document.getElementById("content_table");
var tableHead = table.getElementsByTagName("TR")[0];
var tableItems = tableHead.getElementsByTagName("TD");
for (var h = 0; h < tableItems.length; h++) {
if (tableItems[h].getAttribute("role") == "columnheader") {
tableItems[h].setAttribute("aria-sort", "none");
}
}
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";
tableItems[COLUMN_TO_SORT].classList.remove("sortThis");
tableItems[COLUMN_TO_SORT].classList.add("pointer");
tableItems[column].classList.remove("pointer");
tableItems[column].classList.add("sortThis");
}
COLUMN_TO_SORT = column;
var mobileSort = document.getElementById("mapping-sort-mobile");
if (mobileSort != null && (column == 1 || column == 3 || column == 4 || column == 5)) {
mobileSort.value = column.toString();
}
if (tableItems[column] != undefined && tableItems[column].getAttribute("role") == "columnheader") {
tableItems[column].setAttribute("aria-sort", "ascending");
}
var rows = table.rows;
if (rows[1] != undefined) {
tableHeader = rows[0];
@@ -228,6 +356,7 @@ function createSearchObj() {
var channels = getObjKeys(data);
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"];
channels.forEach(function (id) {
SEARCH_MAPPING[id] = "";
channelKeys.forEach(function (key) {
if (key == "x-active") {
switch (data[id][key]) {
@@ -260,6 +389,9 @@ function searchInMapping() {
for (var i = 1; i < trs.length; ++i) {
var id = trs[i].getAttribute("id");
var element = SEARCH_MAPPING[id];
if (element == undefined) {
continue;
}
switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
case true:
document.getElementById(id).style.display = "";
@@ -269,17 +401,19 @@ function searchInMapping() {
break;
}
}
announceToScreenReader("Search updated");
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;
var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
if (elm != null && content != null) {
var contentTop = content.getBoundingClientRect().top;
var freeSpace = window.innerHeight - contentTop - 26;
if (freeSpace < 180) {
freeSpace = 180;
}
elm.style.height = window.innerHeight - elementsHeight + "px";
elm.style.height = freeSpace + "px";
}
return;
}

View File

@@ -35,7 +35,9 @@ var WizardItem = /** @class */ (function (_super) {
var key = this.key;
var content = new PopupContent();
var description;
var wizardField = null;
var doc = document.getElementById(this.DocumentID);
doc.setAttribute("aria-busy", "true");
doc.innerHTML = "";
doc.appendChild(headline);
switch (key) {
@@ -50,6 +52,7 @@ var WizardItem = /** @class */ (function (_super) {
select.setAttribute("class", "wizard");
select.id = key;
doc.appendChild(select);
wizardField = select;
description = "{{.wizard.tuner.description}}";
break;
case "epgSource":
@@ -59,6 +62,7 @@ var WizardItem = /** @class */ (function (_super) {
select.setAttribute("class", "wizard");
select.id = key;
doc.appendChild(select);
wizardField = select;
description = "{{.wizard.epgSource.description}}";
break;
case "m3u":
@@ -67,6 +71,7 @@ var WizardItem = /** @class */ (function (_super) {
input.setAttribute("class", "wizard");
input.id = key;
doc.appendChild(input);
wizardField = input;
description = "{{.wizard.m3u.description}}";
break;
case "xmltv":
@@ -75,6 +80,7 @@ var WizardItem = /** @class */ (function (_super) {
input.setAttribute("class", "wizard");
input.id = key;
doc.appendChild(input);
wizardField = input;
description = "{{.wizard.xmltv.description}}";
break;
default:
@@ -82,8 +88,20 @@ var WizardItem = /** @class */ (function (_super) {
break;
}
var pre = document.createElement("PRE");
pre.id = "wizard-description-" + key;
pre.innerHTML = description;
doc.appendChild(pre);
if (wizardField != null) {
wizardField.setAttribute("aria-label", this.headline);
wizardField.setAttribute("aria-describedby", pre.id);
setTimeout(function () {
wizardField.focus();
}, 20);
}
doc.setAttribute("aria-busy", "false");
if (typeof announceToScreenReader == "function") {
announceToScreenReader(this.headline + " step");
}
console.log(headline, key);
};
return WizardItem;
@@ -145,3 +163,20 @@ 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}}"));
document.addEventListener("DOMContentLoaded", function () {
var container = document.getElementById("content");
if (container == null) {
return;
}
container.addEventListener("keydown", function (event) {
if (event.key != "Enter") {
return;
}
var target = event.target;
if (target == null || target.tagName != "INPUT") {
return;
}
event.preventDefault();
saveWizard();
});
});

View File

@@ -21,15 +21,22 @@ function showLogs(bottom) {
var log = new Log();
var logs = SERVER["log"]["log"];
var div = document.getElementById("content_log");
var wrapper = document.getElementById("box-wrapper");
var shouldStickToBottom = bottom;
div.innerHTML = "";
if (wrapper != null && shouldStickToBottom == false) {
var distanceToBottom = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight;
if (distanceToBottom < 80) {
shouldStickToBottom = true;
}
}
var keys = getObjKeys(logs);
keys.forEach(function (logID) {
var entry = log.createLog(logs[logID]);
div.append(entry);
});
setTimeout(function () {
if (bottom == true) {
var wrapper = document.getElementById("box-wrapper");
if (shouldStickToBottom == true && wrapper != null) {
wrapper.scrollTop = wrapper.scrollHeight;
}
}, 10);

View File

@@ -43,7 +43,14 @@ var MainMenuItem = /** @class */ (function (_super) {
var item = document.createElement("LI");
item.setAttribute("onclick", "javascript: openThisMenu(this)");
item.setAttribute("id", this.id);
item.setAttribute("data-menu", this.menuKey);
item.setAttribute("role", "menuitem");
item.setAttribute("tabindex", "0");
item.setAttribute("aria-controls", "content");
item.setAttribute("aria-label", this.value);
item.setAttribute("onkeydown", "if(event.key==='Enter' || event.key===' '){event.preventDefault();openThisMenu(this);}");
var img = this.createIMG(this.imgSrc);
img.setAttribute("alt", "");
var value = this.createValue(this.value);
item.appendChild(img);
item.appendChild(value);
@@ -63,7 +70,7 @@ var MainMenuItem = /** @class */ (function (_super) {
this.tableHeader = ["{{.users.table.username}}", "{{.users.table.password}}", "{{.users.table.web}}", "{{.users.table.pms}}", "{{.users.table.m3u}}", "{{.users.table.xml}}", "{{.users.table.api}}"];
break;
case "mapping":
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}"];
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}", "{{.mapping.table.edit}}"];
break;
}
//console.log(this.menuKey, this.tableHeader);
@@ -356,39 +363,27 @@ var Content = /** @class */ (function () {
cell.child = true;
cell.childType = "IMG";
cell.imageURL = data[key]["tvg-logo"];
var td = cell.createCell();
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
tr.appendChild(cell.createCell());
// Kanalname
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.className = data[key]["x-category"];
cell.value = data[key]["x-name"];
var td = cell.createCell();
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
tr.appendChild(cell.createCell());
// Playlist
var cell = new Cell();
cell.child = true;
cell.childType = "P";
//cell.value = data[key]["_file.m3u.name"]
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name");
var td = cell.createCell();
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
tr.appendChild(cell.createCell());
// Gruppe (group-title)
var cell = new Cell();
cell.child = true;
cell.childType = "P";
cell.value = data[key]["x-group-title"];
var td = cell.createCell();
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
tr.appendChild(cell.createCell());
// XMLTV Datei
var cell = new Cell();
cell.child = true;
@@ -399,10 +394,7 @@ var Content = /** @class */ (function () {
else {
cell.value = data[key]["x-xmltv-file"];
}
var td = cell.createCell();
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
tr.appendChild(cell.createCell());
// XMLTV Kanal
var cell = new Cell();
cell.child = true;
@@ -413,10 +405,17 @@ var Content = /** @class */ (function () {
value = data[key]["x-mapping"].substring(0, 20) + "...";
}
cell.value = value;
var td = cell.createCell();
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
tr.appendChild(cell.createCell());
var cell = new Cell();
cell.child = true;
cell.childType = "EDIT";
cell.value = "{{.button.edit}}";
var editTd = cell.createCell();
var editButton = editTd.firstChild;
editButton.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
editButton.setAttribute("id", key);
editButton.setAttribute("aria-label", "Edit " + data[key]["x-name"]);
tr.appendChild(editTd);
rows.push(tr);
});
break;
@@ -451,7 +450,7 @@ var Cell = /** @class */ (function () {
break;
case "INPUTCHANNEL":
element = document.createElement("INPUT");
element.setAttribute("onchange", "javscript: changeChannelNumber(this)");
element.setAttribute("onchange", "javascript: changeChannelNumber(this)");
element.value = this.value;
element.type = "text";
break;
@@ -460,6 +459,9 @@ var Cell = /** @class */ (function () {
element.checked = this.value;
element.type = "checkbox";
element.className = "bulk hideBulk";
element.addEventListener("click", function (event) {
scheduleChannelRangeSelection(element, event);
});
break;
case "BULK_HEAD":
element = document.createElement("INPUT");
@@ -475,6 +477,12 @@ var Cell = /** @class */ (function () {
element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''");
//onerror="this.onerror=null;this.src='missing.gif';"
}
break;
case "EDIT":
element = document.createElement("INPUT");
element.type = "button";
element.value = this.value;
element.className = "mapping-edit-button";
}
td.appendChild(element);
}
@@ -483,10 +491,18 @@ var Cell = /** @class */ (function () {
}
if (this.onclick == true) {
td.setAttribute("onclick", this.onclickFunktion);
td.className = "pointer";
td.className = "pointer keyboard-clickable";
td.setAttribute("tabindex", "0");
td.setAttribute("role", "button");
td.setAttribute("onkeydown", "if(event.key==='Enter' || event.key===' '){event.preventDefault();this.click();}");
}
if (this.tdClassName != undefined) {
td.className = this.tdClassName;
if (td.className.length > 0) {
td.className = td.className + " " + this.tdClassName;
}
else {
td.className = this.tdClassName;
}
}
return td;
};
@@ -510,6 +526,7 @@ var ShowContent = /** @class */ (function (_super) {
COLUMN_TO_SORT = -1;
// Alten Inhalt löschen
var doc = document.getElementById(this.DocumentID);
doc.setAttribute("aria-busy", "true");
doc.innerHTML = "";
showPreview(false);
// Überschrift
@@ -556,11 +573,33 @@ var ShowContent = /** @class */ (function (_super) {
var input = this.createInput("button", menuKey, "{{.button.bulkEdit}}");
input.setAttribute("onclick", 'javascript: bulkEdit()');
interaction.appendChild(input);
var sortSelect = document.createElement("SELECT");
sortSelect.setAttribute("id", "mapping-sort-mobile");
sortSelect.className = "mobile-only-control";
sortSelect.setAttribute("aria-label", "Sort mapping");
var sortOptions = [
{ label: "{{.mapping.table.chNo}}", value: "1" },
{ label: "{{.mapping.table.channelName}}", value: "3" },
{ label: "{{.mapping.table.playlist}}", value: "4" },
{ label: "{{.mapping.table.groupTitle}}", value: "5" }
];
sortOptions.forEach(function (optionData) {
var option = document.createElement("OPTION");
option.innerText = optionData.label;
option.value = optionData.value;
sortSelect.appendChild(option);
});
sortSelect.value = "1";
sortSelect.onchange = function () {
sortTable(parseInt(this.value, 10));
};
interaction.appendChild(sortSelect);
var input = this.createInput("search", "search", "");
input.setAttribute("id", "searchMapping");
input.setAttribute("placeholder", "{{.button.search}}");
input.setAttribute("aria-label", "{{.button.search}}");
input.className = "search";
input.setAttribute("onchange", 'javascript: searchInMapping()');
input.setAttribute("oninput", 'javascript: searchInMapping()');
interaction.appendChild(input);
break;
case "settings":
@@ -580,6 +619,7 @@ var ShowContent = /** @class */ (function (_super) {
var settings = this.createDIV();
wrapper.appendChild(settings);
showSettings();
finalizeContentAccessibility(headline);
return;
break;
case "log":
@@ -593,6 +633,7 @@ var ShowContent = /** @class */ (function (_super) {
var logs = this.createDIV();
wrapper.appendChild(logs);
showLogs(true);
finalizeContentAccessibility(headline);
return;
break;
case "logout":
@@ -665,30 +706,392 @@ var ShowContent = /** @class */ (function (_super) {
break;
}
showElement("loading", false);
finalizeContentAccessibility(headline);
};
return ShowContent;
}(Content));
var SHELL_LAYOUT_READY = false;
function isKeyboardActivationKey(event) {
return event.key == "Enter" || event.key == " ";
}
function makeKeyboardClickable(element, label) {
if (element == null) {
return;
}
if (element.getAttribute("data-keyboard-ready") == "true") {
return;
}
var tagName = element.tagName.toUpperCase();
if (tagName == "INPUT" || tagName == "BUTTON" || tagName == "SELECT" || tagName == "TEXTAREA" || tagName == "A") {
return;
}
element.setAttribute("data-keyboard-ready", "true");
if (element.getAttribute("tabindex") == null) {
element.setAttribute("tabindex", "0");
}
if (element.getAttribute("role") == null) {
element.setAttribute("role", "button");
}
element.classList.add("keyboard-clickable");
if (label != undefined && label.length > 0) {
if (element.getAttribute("aria-label") == null || element.getAttribute("aria-label").length == 0) {
element.setAttribute("aria-label", label);
}
}
element.addEventListener("keydown", function (event) {
if (isKeyboardActivationKey(event) == false) {
return;
}
event.preventDefault();
this.click();
});
}
function applyTableAccessibility(table, sectionName) {
if (table == null) {
return;
}
table.setAttribute("role", "table");
var rows = table.getElementsByTagName("TR");
var headerLabels = [];
if (rows.length > 0) {
var headerCells = rows[0].getElementsByTagName("TD");
for (var h = 0; h < headerCells.length; h++) {
var headerLabel = headerCells[h].innerText;
if (headerLabel == undefined || headerLabel.length == 0) {
headerLabel = "Value";
}
if (headerLabel == "BULK") {
headerLabel = "Select";
}
headerLabels.push(headerLabel);
}
}
for (var i = 0; i < rows.length; i++) {
rows[i].setAttribute("role", "row");
if (rows[i].getAttribute("onclick") != null) {
var rowLabel = rows[i].innerText;
if (rowLabel == undefined || rowLabel.length == 0) {
rowLabel = sectionName + " row";
}
makeKeyboardClickable(rows[i], rowLabel);
}
var cells = rows[i].getElementsByTagName("TD");
for (var c = 0; c < cells.length; c++) {
if (i == 0) {
cells[c].setAttribute("role", "columnheader");
}
else {
cells[c].setAttribute("role", "cell");
}
var dataLabel = headerLabels[c];
if (dataLabel == undefined || dataLabel.length == 0) {
dataLabel = "Value";
}
cells[c].setAttribute("data-label", dataLabel);
var checkbox = cells[c].querySelector('input[type="checkbox"]');
if (checkbox != null) {
cells[c].setAttribute("data-cell-type", "checkbox");
}
else {
var image = cells[c].querySelector("img");
if (image != null) {
cells[c].setAttribute("data-cell-type", "image");
}
else {
var actionButton = cells[c].querySelector("input.mapping-edit-button, button.mapping-edit-button");
if (actionButton != null) {
cells[c].setAttribute("data-cell-type", "action");
}
else {
cells[c].removeAttribute("data-cell-type");
}
}
}
if (cells[c].getAttribute("onclick") != null) {
var cellLabel = cells[c].innerText;
if (cellLabel == undefined || cellLabel.length == 0) {
cellLabel = sectionName + " details";
}
makeKeyboardClickable(cells[c], cellLabel);
}
}
}
}
function finalizeContentAccessibility(sectionName) {
var content = document.getElementById("content");
if (content == null) {
return;
}
content.setAttribute("aria-busy", "false");
var heading = content.getElementsByTagName("H3")[0];
if (heading != null) {
heading.setAttribute("tabindex", "-1");
setTimeout(function () {
heading.focus();
}, 20);
}
applyTableAccessibility(document.getElementById("content_table"), sectionName);
if (sectionName != undefined && sectionName.length > 0) {
announceToScreenReader(sectionName + " loaded");
}
}
function setLayoutMenuState(open) {
if (document.body == null) {
return;
}
if (open == true) {
document.body.classList.add("menu-open");
}
else {
document.body.classList.remove("menu-open");
}
var toggle = document.getElementById("menu-toggle");
if (toggle != null) {
toggle.setAttribute("aria-expanded", open == true ? "true" : "false");
toggle.setAttribute("aria-label", open == true ? "Close navigation menu" : "Open navigation menu");
}
var overlay = document.getElementById("layout-overlay");
if (overlay != null) {
overlay.setAttribute("aria-hidden", open == true ? "false" : "true");
}
var wrapper = document.getElementById("menu-wrapper");
if (wrapper != null) {
if (window.innerWidth <= 900) {
wrapper.setAttribute("aria-hidden", open == true ? "false" : "true");
}
else {
wrapper.setAttribute("aria-hidden", "false");
}
}
if (window.innerWidth <= 900 && open == false && toggle != null && wrapper != null && wrapper.contains(document.activeElement)) {
toggle.focus();
}
if (window.innerWidth <= 900 && open == true && wrapper != null) {
var firstMenuItem = wrapper.querySelector("#main-menu li");
if (firstMenuItem != null) {
setTimeout(function () {
firstMenuItem.focus();
}, 30);
}
}
}
function toggleLayoutMenu() {
if (document.body == null) {
return;
}
var isOpen = document.body.classList.contains("menu-open");
setLayoutMenuState(!isOpen);
}
function closeLayoutMenuIfMobile() {
if (window.innerWidth <= 900) {
setLayoutMenuState(false);
}
}
function setActiveMenu(menuID) {
ACTIVE_MENU_ID = menuID.toString();
var menu = document.getElementById("main-menu");
if (menu == null) {
return;
}
var items = menu.getElementsByTagName("LI");
for (var i = 0; i < items.length; i++) {
items[i].classList.remove("menu-active");
items[i].removeAttribute("aria-current");
}
var activeItem = document.getElementById(ACTIVE_MENU_ID);
var activeMenuKey = "";
if (activeItem != null) {
activeItem.classList.add("menu-active");
activeItem.setAttribute("aria-current", "page");
var menuKeyValue = activeItem.getAttribute("data-menu");
if (menuKeyValue != null) {
activeMenuKey = menuKeyValue;
}
}
if (document.body != null) {
if (activeMenuKey.length > 0) {
document.body.setAttribute("data-active-menu", activeMenuKey);
}
else {
document.body.removeAttribute("data-active-menu");
}
if (activeMenuKey == "log") {
document.body.classList.add("menu-log-focus");
}
else {
document.body.classList.remove("menu-log-focus");
}
}
}
function renderStatusCards() {
var wrapper = document.getElementById("status-cards");
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
return;
}
var info = SERVER["clientInfo"];
var errors = parseInt(info["errors"], 10);
var warnings = parseInt(info["warnings"], 10);
var cards = [
{ label: "Streams", value: info["streams"], tone: "ok" },
{ label: "EPG Source", value: info["epgSource"], tone: "neutral" },
{ label: "XEPG Channels", value: info["xepg"], tone: "ok" },
{ label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok" },
{ label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok" },
{ label: "DVR", value: info["DVR"], tone: "neutral" }
];
wrapper.innerHTML = "";
cards.forEach(function (card) {
var box = document.createElement("DIV");
box.className = "status-card status-card-" + card.tone;
box.setAttribute("role", "listitem");
var label = document.createElement("P");
label.className = "status-card-label";
label.innerText = card.label;
var value = document.createElement("P");
value.className = "status-card-value";
if (card.value == undefined || card.value == "") {
value.innerText = "-";
}
else {
value.innerText = card.value;
}
box.appendChild(label);
box.appendChild(value);
box.setAttribute("aria-label", card.label + ": " + value.innerText);
wrapper.appendChild(box);
});
}
function initShellLayout() {
if (SHELL_LAYOUT_READY == true) {
return;
}
var toggle = document.getElementById("menu-toggle");
if (toggle != null) {
toggle.onclick = function () {
toggleLayoutMenu();
};
}
var overlay = document.getElementById("layout-overlay");
if (overlay != null) {
overlay.onclick = function () {
setLayoutMenuState(false);
};
}
document.addEventListener("keydown", function (event) {
if (event.key == "Escape") {
setLayoutMenuState(false);
showElement("popup", false);
return;
}
if (event.key == "/") {
var target = event.target;
var tagName = target != null && target.tagName != undefined ? target.tagName : "";
var onInput = tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
if (onInput == true) {
return;
}
var search = document.getElementById("searchMapping");
if (search != null) {
event.preventDefault();
search.focus();
}
}
});
setLayoutMenuState(false);
setConnectionState("idle");
SHELL_LAYOUT_READY = true;
}
function shouldPollLogs() {
if (document.hidden == true) {
return false;
}
if (document.getElementById("content_log") == null) {
return false;
}
if (ACTIVE_MENU_ID.length < 1) {
return false;
}
var activeItem = document.getElementById(ACTIVE_MENU_ID);
if (activeItem == null) {
return false;
}
return activeItem.getAttribute("data-menu") == "log";
}
function PageReady() {
initShellLayout();
var server = new Server("getServerConfig");
server.request(new Object());
var bootstrapAttempts = 0;
var maxBootstrapAttempts = 5;
var bootstrapTimer = window.setInterval(function () {
if (SERVER.hasOwnProperty("clientInfo") == true) {
window.clearInterval(bootstrapTimer);
return;
}
if (SERVER_CONNECTION == true) {
return;
}
bootstrapAttempts++;
var retryServer = new Server("getServerConfig");
retryServer.request(new Object());
if (bootstrapAttempts >= maxBootstrapAttempts) {
window.clearInterval(bootstrapTimer);
}
}, 3000);
window.addEventListener("resize", function () {
if (window.innerWidth > 900) {
setLayoutMenuState(false);
}
calculateWrapperHeight();
}, true);
setInterval(function () {
if (shouldPollLogs() == false) {
return;
}
updateLog();
}, 10000);
return;
}
function isClientInfoHttpURL(value) {
return /^https?:\/\//i.test(value);
}
function setClientInfoValue(key, value) {
var element = document.getElementById(key);
if (element == null) {
return;
}
var textValue = "";
if (value != undefined && value != null) {
textValue = String(value);
}
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
element.innerHTML = "";
var anchor = document.createElement("A");
anchor.href = textValue;
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.textContent = textValue;
element.appendChild(anchor);
return;
}
element.innerHTML = textValue;
}
function createLayout() {
var contentRegion = document.getElementById("content");
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "true");
}
// Client Info
var obj = SERVER["clientInfo"];
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]];
}
setClientInfoValue(keys[i], obj[keys[i]]);
}
renderStatusCards();
if (!document.getElementById("main-menu")) {
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "false");
}
return;
}
// Menü erstellen
@@ -713,12 +1116,35 @@ function createLayout() {
break;
}
}
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
setActiveMenu(ACTIVE_MENU_ID);
}
setLayoutMenuState(document.body.classList.contains("menu-open"));
var content = document.getElementById("content");
var menu = document.getElementById("main-menu");
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
var firstItem = menu.getElementsByTagName("LI")[0];
if (firstItem != undefined) {
firstItem.click();
}
}
}
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "false");
}
return;
}
function openThisMenu(element) {
var id = element.id;
var content = new ShowContent(id);
setActiveMenu(id);
content.show();
var contentArea = document.getElementById("content");
if (contentArea != null) {
contentArea.scrollTop = 0;
}
closeLayoutMenuIfMobile();
calculateWrapperHeight();
return;
}
@@ -756,9 +1182,26 @@ var PopupContent = /** @class */ (function (_super) {
}
PopupContent.prototype.createHeadline = function (headline) {
this.doc.innerHTML = "";
var titleBar = document.createElement("DIV");
titleBar.className = "popup-title";
var element = document.createElement("H3");
element.id = "popup-title-text";
element.innerHTML = headline.toUpperCase();
this.doc.appendChild(element);
titleBar.appendChild(element);
var closeButton = document.createElement("BUTTON");
closeButton.setAttribute("type", "button");
closeButton.className = "popup-close";
closeButton.setAttribute("aria-label", "Close dialog");
closeButton.innerHTML = "&times;";
closeButton.onclick = function () {
showElement("popup", false);
};
titleBar.appendChild(closeButton);
this.doc.appendChild(titleBar);
var popup = document.getElementById("popup");
if (popup != null) {
popup.setAttribute("aria-labelledby", "popup-title-text");
}
// Tabelle erstellen
this.table = document.createElement("TABLE");
this.doc.appendChild(this.table);

View File

@@ -11,6 +11,7 @@ var Server = /** @class */ (function () {
if (this.cmd != "updateLog") {
showElement("loading", true);
UNDO = new Object();
setConnectionState("busy");
}
switch (window.location.protocol) {
case "http:":
@@ -20,11 +21,61 @@ var Server = /** @class */ (function () {
this.protocol = "wss://";
break;
}
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token");
var wsHost = window.location.host;
if (wsHost == undefined || wsHost.length < 1) {
wsHost = window.location.hostname;
}
var url = this.protocol + wsHost + "/data/" + "?Token=" + getCookie("Token");
data["cmd"] = this.cmd;
var requestCmd = data["cmd"];
var ws = new WebSocket(url);
var isLogUpdate = data["cmd"] == "updateLog";
var responseReceived = false;
var requestFinished = false;
var timeoutMs = 12000;
var requestTimeout;
var finishRequest = function (state, responseSuccess) {
if (responseSuccess === void 0) { responseSuccess = false; }
if (requestFinished == true) {
return;
}
requestFinished = true;
SERVER_CONNECTION = false;
window.clearTimeout(requestTimeout);
if (responseSuccess == true) {
if (state == "online") {
WS_FAILURE_COUNT = 0;
}
}
else {
WS_FAILURE_COUNT++;
}
if (isLogUpdate == false) {
showElement("loading", false);
}
if (state != "") {
setConnectionState(state);
}
};
requestTimeout = window.setTimeout(function () {
console.log("Websocket request timed out.");
var timeoutState = "offline";
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
timeoutState = "idle";
}
finishRequest(timeoutState, false);
try {
ws.close();
}
catch (err) {
console.log(err);
}
}, timeoutMs);
ws.onopen = function () {
WS_AVAILABLE = true;
if (data["cmd"] != "updateLog") {
setConnectionState("busy");
}
console.log("REQUEST (JS):");
console.log(data);
console.log("REQUEST: (JSON)");
@@ -33,14 +84,18 @@ var Server = /** @class */ (function () {
};
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) {
var errorState = "offline";
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
errorState = "idle";
}
finishRequest(errorState, false);
if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
alert("No websocket connection to xTeVe could be established. Check your network configuration.");
}
};
ws.onmessage = function (e) {
SERVER_CONNECTION = false;
showElement("loading", false);
responseReceived = true;
finishRequest("online", true);
console.log("RESPONSE:");
var response = JSON.parse(e.data);
console.log(response);
@@ -48,6 +103,7 @@ var Server = /** @class */ (function () {
document.cookie = "Token=" + response["token"];
}
if (response["status"] == false) {
setConnectionState("offline");
alert(response["err"]);
if (response.hasOwnProperty("reload")) {
location.reload();
@@ -94,9 +150,20 @@ var Server = /** @class */ (function () {
}
createLayout();
};
ws.onclose = function () {
if (responseReceived == true) {
return;
}
var closeState = "offline";
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
closeState = "idle";
}
finishRequest(closeState, false);
};
};
return Server;
}());
var WS_FAILURE_COUNT = 0;
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");

View File

@@ -74,6 +74,29 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "plex.url":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.plexURL.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "plex.url", data);
input.setAttribute("placeholder", "{{.settings.plexURL.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "plex.token":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.plexToken.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("password", "plex.token", data);
input.setAttribute("placeholder", "{{.settings.plexToken.placeholder}}");
input.setAttribute("autocomplete", "off");
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}}" + ":";
@@ -240,7 +263,30 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "use_plexAPI":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.usePlexAPI.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 "xepg.missing.epg.mode":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.xepgMissingEPGMode.title}}" + ":";
var tdRight = document.createElement("TD");
var text = ["{{.settings.xepgMissingEPGMode.info_strict}}", "{{.settings.xepgMissingEPGMode.info_relaxed}}"];
var values = ["strict", "relaxed"];
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 "tuner":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":";
@@ -364,6 +410,12 @@ var SettingsCategory = /** @class */ (function () {
case "user.agent":
text = "{{.settings.userAgent.description}}";
break;
case "plex.url":
text = "{{.settings.plexURL.description}}";
break;
case "plex.token":
text = "{{.settings.plexToken.description}}";
break;
case "ffmpeg.path":
text = "{{.settings.ffmpegPath.description}}";
break;
@@ -388,6 +440,9 @@ var SettingsCategory = /** @class */ (function () {
case "api":
text = "{{.settings.api.description}}";
break;
case "use_plexAPI":
text = "{{.settings.usePlexAPI.description}}";
break;
case "files.update":
text = "{{.settings.filesUpdate.description}}";
break;
@@ -397,6 +452,9 @@ var SettingsCategory = /** @class */ (function () {
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}";
break;
case "xepg.missing.epg.mode":
text = "{{.settings.xepgMissingEPGMode.description}}";
break;
case "udpxy":
text = "{{.settings.udpxy.description}}";
break;

View File

@@ -41,6 +41,7 @@
"backup": "Backup",
"bulkEdit": "Bulk Edit",
"cancel": "Cancel",
"edit": "Edit",
"delete": "Delete",
"done": "Done",
"login": "Login",
@@ -210,7 +211,8 @@
"playlist": "Playlist",
"groupTitle": "Group Title",
"xmltvFile": "XMLTV File",
"xmltvID": "XMLTV ID"
"xmltvID": "XMLTV ID",
"edit": "Edit"
},
"active":
{
@@ -355,6 +357,23 @@
"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>"
},
"usePlexAPI":
{
"title": "Use Plex API Refresh",
"description": "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates."
},
"plexURL":
{
"title": "Plex Server URL",
"description": "Base URL of your Plex server. Example: http://192.168.1.10:32400",
"placeholder": "http://plex-host:32400"
},
"plexToken":
{
"title": "Plex API Token",
"description": "Plex token used for authenticated API calls to refresh your DVR guide.",
"placeholder": "Plex X-Plex-Token"
},
"epgSource":
{
"title": "EPG Source",
@@ -380,6 +399,13 @@
"title": "Replace missing program images",
"description": "If the poster in the XMLTV program is missing, the channel logo will be used."
},
"xepgMissingEPGMode":
{
"title": "Missing EPG Handling",
"description": "Strict: channels are deactivated when XMLTV mappings disappear.<br>Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved.",
"info_strict": "Strict (deactivate channels)",
"info_relaxed": "Relaxed (keep active / dummy guide)"
},
"xteveAutoUpdate":
{
"title": "Automatic update of xTeVe",

View File

@@ -10,36 +10,36 @@
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>
<div id="box">
<main id="box" role="main" aria-labelledby="head-text">
<div id="headline">
<h1 id="head-text" class="center">{{.login.headline}}</h1>
</div>
<p id="err" class="errorMsg center">{{.authenticationErr}}</p>
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true">{{.authenticationErr}}</p>
<div id="content">
<form id="authentication" action="" method="post">
<form id="authentication" action="" method="post" aria-describedby="err" novalidate>
<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="">
<label for="username">{{.login.username.title}}:</label>
<input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
<label for="password">{{.login.password.title}}:</label>
<input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="current-password">
</form>
</div>
<div id="box-footer">
<input id="submit" class="" type="button" value="{{.button.login}}" onclick="javascript: login();">
<input id="submit" class="" type="button" value="{{.button.login}}" aria-label="{{.button.login}}" onclick="javascript: login();">
</div>
</div>
</main>
</body>

View File

@@ -8,7 +8,7 @@
<link rel="stylesheet" href="css/base.css" type="text/css">
</head>
<body>
<body class="auth-screen">
<div id="header" class="imgCenter"></div>

View File

@@ -16,7 +16,7 @@ func activatedSystemAuthentication() (err error) {
return
}
var defaults = make(map[string]interface{})
var defaults = make(map[string]any)
defaults["authentication.web"] = false
defaults["authentication.pms"] = false
defaults["authentication.xml"] = false
@@ -43,7 +43,7 @@ func createFirstUserForAuthentication(username, password string) (token string,
token, err = authentication.CheckTheValidityOfTheToken(token)
authenticationErr(err)
var userData = make(map[string]interface{})
var userData = make(map[string]any)
userData["username"] = username
userData["authentication.web"] = true
userData["authentication.pms"] = true

View File

@@ -657,7 +657,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
Redirect:
req, err := http.NewRequest("GET", currentURL, nil)
req.Header.Set("User-Agent", Settings.UserAgent)
req.Header.Set("User-Agent", getUserAgent())
req.Header.Set("Connection", "close")
//req.Header.Set("Range", "bytes=0-")
req.Header.Set("Accept", "*/*")
@@ -1423,14 +1423,16 @@ func thirdPartyBuffer(streamID int, playlistID string) {
// User-Agent setzen
var args []string
var userAgent = getUserAgent()
for i, a := range strings.Split(options, " ") {
switch bufferType {
case "FFMPEG":
a = strings.Replace(a, "[URL]", url, -1)
if i == 0 {
if len(Settings.UserAgent) != 0 {
args = []string{"-user_agent", Settings.UserAgent}
if len(userAgent) != 0 {
args = []string{"-user_agent", userAgent}
}
}
@@ -1441,8 +1443,8 @@ func thirdPartyBuffer(streamID int, playlistID string) {
a = strings.Replace(a, "[URL]", url, -1)
args = append(args, a)
if len(Settings.UserAgent) != 0 {
args = append(args, fmt.Sprintf(":http-user-agent=%s", Settings.UserAgent))
if len(userAgent) != 0 {
args = append(args, fmt.Sprintf(":http-user-agent=%s", userAgent))
}
} else {

View File

@@ -23,6 +23,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
var reloadData = false
var cacheImages = false
var createXEPGFiles = false
var triggerPlexGuideReload = false
var debug string
// -vvv [URL] --sout '#transcode{vcodec=mp4v, acodec=mpga} :standard{access=http, mux=ogg}'
@@ -36,6 +37,21 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
case "tuner":
showWarning(2105)
case "use_plexAPI":
triggerPlexGuideReload = true
case "plex.url":
if v, ok := value.(string); ok {
value = strings.TrimRight(strings.TrimSpace(v), "/")
}
triggerPlexGuideReload = true
case "plex.token":
if v, ok := value.(string); ok {
value = strings.TrimSpace(v)
}
triggerPlexGuideReload = true
case "epgSource":
reloadData = true
@@ -43,7 +59,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
// Leerzeichen aus den Werten entfernen und Formatierung der Uhrzeit überprüfen (0000 - 2359)
var newUpdateTimes = make([]string, 0)
for _, v := range value.([]interface{}) {
for _, v := range value.([]any) {
v = strings.Replace(v.(string), " ", "", -1)
@@ -69,6 +85,16 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
case "xepg.replace.missing.images":
createXEPGFiles = true
case "xepg.missing.epg.mode":
if v, ok := value.(string); ok {
mode := strings.ToLower(strings.TrimSpace(v))
if mode != "relaxed" {
mode = "strict"
}
value = mode
}
reloadData = true
case "backup.path":
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
err = checkFolder(value.(string))
@@ -119,22 +145,26 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
oldSettings[key] = value
switch fmt.Sprintf("%T", value) {
if key == "plex.token" {
debug = fmt.Sprintf("Save Setting:Key: %s | Value: ******** (%T)", key, value)
} else {
switch fmt.Sprintf("%T", value) {
case "bool":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, 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 "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 "[]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)
case "float64":
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
default:
debug = fmt.Sprintf("%T", value)
default:
debug = fmt.Sprintf("%T", value)
}
}
showDebug(debug, 1)
@@ -250,6 +280,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
}
if triggerPlexGuideReload == true {
queuePlexGuideRefresh("settings change")
}
}
return
@@ -258,8 +292,8 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
// Providerdaten speichern (WebUI)
func saveFiles(request RequestStruct, fileType string) (err error) {
var filesMap = make(map[string]interface{})
var newData = make(map[string]interface{})
var filesMap = make(map[string]any)
var newData = make(map[string]any)
var indicator string
var reloadData = false
@@ -281,7 +315,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
}
if len(filesMap) == 0 {
filesMap = make(map[string]interface{})
filesMap = make(map[string]any)
}
for dataID, data := range newData {
@@ -290,15 +324,15 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
// Neue Providerdatei
dataID = indicator + randomString(19)
data.(map[string]interface{})["new"] = true
data.(map[string]any)["new"] = true
filesMap[dataID] = data
} else {
// Bereits vorhandene Providerdatei
for key, value := range data.(map[string]interface{}) {
for key, value := range data.(map[string]any) {
var oldData = filesMap[dataID].(map[string]interface{})
var oldData = filesMap[dataID].(map[string]any)
oldData[key] = value
}
@@ -319,11 +353,11 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
}
// Neue Providerdatei
if _, ok := data.(map[string]interface{})["new"]; ok {
if _, ok := data.(map[string]any)["new"]; ok {
reloadData = true
err = getProviderData(fileType, dataID)
delete(data.(map[string]interface{}), "new")
delete(data.(map[string]any), "new")
if err != nil {
delete(filesMap, dataID)
@@ -332,7 +366,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
}
if _, ok := data.(map[string]interface{})["delete"]; ok {
if _, ok := data.(map[string]any)["delete"]; ok {
deleteLocalProviderFiles(dataID, fileType)
reloadData = true
@@ -365,7 +399,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
// Providerdaten manuell aktualisieren (WebUI)
func updateFile(request RequestStruct, fileType string) (err error) {
var updateData = make(map[string]interface{})
var updateData = make(map[string]any)
switch fileType {
@@ -395,7 +429,7 @@ func updateFile(request RequestStruct, fileType string) (err error) {
// Providerdaten löschen (WebUI)
func deleteLocalProviderFiles(dataID, fileType string) {
var removeData = make(map[string]interface{})
var removeData = make(map[string]any)
var fileExtension string
switch fileType {
@@ -424,8 +458,8 @@ func deleteLocalProviderFiles(dataID, fileType string) {
// Filtereinstellungen speichern (WebUI)
func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
var filterMap = make(map[int64]interface{})
var newData = make(map[int64]interface{})
var filterMap = make(map[int64]any)
var newData = make(map[int64]any)
var defaultFilter FilterStruct
var newFilter = false
@@ -458,15 +492,15 @@ func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
}
// Filter aktualisieren / löschen
for key, value := range data.(map[string]interface{}) {
for key, value := range data.(map[string]any) {
// Filter löschen
if _, ok := data.(map[string]interface{})["delete"]; ok {
if _, ok := data.(map[string]any)["delete"]; ok {
delete(filterMap, dataID)
break
}
if filter, ok := data.(map[string]interface{})["filter"].(string); ok {
if filter, ok := data.(map[string]any)["filter"].(string); ok {
if len(filter) == 0 {
@@ -480,7 +514,7 @@ func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
}
if oldData, ok := filterMap[dataID].(map[string]interface{}); ok {
if oldData, ok := filterMap[dataID].(map[string]any); ok {
oldData[key] = value
}
@@ -573,7 +607,7 @@ func saveUserData(request RequestStruct) (err error) {
var userData = request.UserData
var newCredentials = func(userID string, newUserData map[string]interface{}) (err error) {
var newCredentials = func(userID string, newUserData map[string]any) (err error) {
var newUsername, newPassword string
if username, ok := newUserData["username"].(string); ok {
@@ -593,7 +627,7 @@ func saveUserData(request RequestStruct) (err error) {
for userID, newUserData := range userData {
err = newCredentials(userID, newUserData.(map[string]interface{}))
err = newCredentials(userID, newUserData.(map[string]any))
if err != nil {
return
}
@@ -603,16 +637,16 @@ func saveUserData(request RequestStruct) (err error) {
return
}
delete(newUserData.(map[string]interface{}), "password")
delete(newUserData.(map[string]interface{}), "confirm")
delete(newUserData.(map[string]any), "password")
delete(newUserData.(map[string]any), "confirm")
if _, ok := newUserData.(map[string]interface{})["delete"]; ok {
if _, ok := newUserData.(map[string]any)["delete"]; ok {
authentication.RemoveUser(userID)
} else {
err = authentication.WriteUserData(userID, newUserData.(map[string]interface{}))
err = authentication.WriteUserData(userID, newUserData.(map[string]any))
if err != nil {
return
}
@@ -662,11 +696,11 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
case "m3u", "xmltv":
var filesMap = make(map[string]interface{})
var data = make(map[string]interface{})
var filesMap = make(map[string]any)
var data = make(map[string]any)
var indicator, dataID string
filesMap = make(map[string]interface{})
filesMap = make(map[string]any)
data["type"] = key
data["new"] = true
@@ -737,6 +771,10 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
}
if nextStep == 10 {
Settings.WizardCompleted = true
}
err = saveSettings(Settings)
if err != nil {
return
@@ -797,9 +835,9 @@ func buildDatabaseDVR() (err error) {
System.ScanInProgress = 1
Data.Streams.All = make([]interface{}, 0, System.UnfilteredChannelLimit)
Data.Streams.Active = make([]interface{}, 0, System.UnfilteredChannelLimit)
Data.Streams.Inactive = make([]interface{}, 0, System.UnfilteredChannelLimit)
Data.Streams.All = make([]any, 0, System.UnfilteredChannelLimit)
Data.Streams.Active = make([]any, 0, System.UnfilteredChannelLimit)
Data.Streams.Inactive = make([]any, 0, System.UnfilteredChannelLimit)
Data.Playlist.M3U.Groups.Text = []string{}
Data.Playlist.M3U.Groups.Value = []string{}
Data.StreamPreviewUI.Active = []string{}
@@ -820,7 +858,7 @@ func buildDatabaseDVR() (err error) {
for n, i := range playlistFile {
var channels []interface{}
var channels []any
var groupTitle, tvgID, uuid int = 0, 0, 0
var keys = []string{"group-title", "tvg-id", "uuid"}
var compatibility = make(map[string]int)
@@ -957,7 +995,7 @@ func buildDatabaseDVR() (err error) {
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.UnfilteredChannelLimit && len(Settings.Filter) == 0 {
Data.Streams.Active = Data.Streams.All
Data.Streams.Inactive = make([]interface{}, 0)
Data.Streams.Inactive = make([]any, 0)
Data.StreamPreviewUI.Active = Data.StreamPreviewUI.Inactive
Data.StreamPreviewUI.Inactive = []string{}
@@ -980,6 +1018,10 @@ func buildDatabaseDVR() (err error) {
sort.Strings(Data.StreamPreviewUI.Active)
sort.Strings(Data.StreamPreviewUI.Inactive)
if Settings.EpgSource != "XEPG" {
queuePlexGuideRefresh("lineup update")
}
return
}
@@ -987,7 +1029,7 @@ func buildDatabaseDVR() (err error) {
func getLocalProviderFiles(fileType string) (localFiles []string) {
var fileExtension string
var dataMap = make(map[string]interface{})
var dataMap = make(map[string]any)
switch fileType {
@@ -1015,7 +1057,7 @@ func getLocalProviderFiles(fileType string) (localFiles []string) {
// Providerparameter anhand von dem Key ausgeben
func getProviderParameter(id, fileType, key string) (s string) {
var dataMap = make(map[string]interface{})
var dataMap = make(map[string]any)
switch fileType {
case "m3u":
@@ -1028,7 +1070,7 @@ func getProviderParameter(id, fileType, key string) (s string) {
dataMap = Settings.Files.XMLTV
}
if data, ok := dataMap[id].(map[string]interface{}); ok {
if data, ok := dataMap[id].(map[string]any); ok {
if v, ok := data[key].(string); ok {
s = v
@@ -1046,7 +1088,7 @@ func getProviderParameter(id, fileType, key string) (s string) {
// Provider Statistiken Kompatibilität aktualisieren
func setProviderCompatibility(id, fileType string, compatibility map[string]int) {
var dataMap = make(map[string]interface{})
var dataMap = make(map[string]any)
switch fileType {
case "m3u":
@@ -1059,7 +1101,7 @@ func setProviderCompatibility(id, fileType string, compatibility map[string]int)
dataMap = Settings.Files.XMLTV
}
if data, ok := dataMap[id].(map[string]interface{}); ok {
if data, ok := dataMap[id].(map[string]any); ok {
data["compatibility"] = compatibility

View File

@@ -7,9 +7,9 @@ import (
"fmt"
)
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []interface{}, err error) {
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []any, err error) {
var hdhrData []interface{}
var hdhrData []any
err = json.Unmarshal(content, &hdhrData)
if err == nil {
@@ -17,7 +17,7 @@ func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []i
for _, d := range hdhrData {
var channel = make(map[string]string)
var data = d.(map[string]interface{})
var data = d.(map[string]any)
channel["group-title"] = playlistName
channel["name"] = data["GuideName"].(string)

View File

@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
)
var htmlFolder string
@@ -16,7 +17,7 @@ var goFile string
var mapName string
var packageName string
var blankMap = make(map[string]interface{})
var blankMap = make(map[string]any)
// HTMLInit : Dateipfade festlegen
// mapName = Name der zu erstellenden map
@@ -68,14 +69,14 @@ func createMapFromFiles(folder string) string {
checkErr(err)
}
var content string
var content strings.Builder
for key := range blankMap {
var newKey = key
content += ` ` + mapName + `["` + newKey + `"` + `] = "` + blankMap[key].(string) + `"` + "\n"
content.WriteString(` ` + mapName + `["` + newKey + `"` + `] = "` + blankMap[key].(string) + `"` + "\n")
}
return content
return content.String()
}
func readFilesToMap(path string, info os.FileInfo, err error) error {

View File

@@ -1,100 +1,101 @@
package src
import (
"fmt"
"strings"
"fmt"
"strings"
)
// ShowSystemInfo : Systeminformationen anzeigen
func ShowSystemInfo() {
fmt.Print("Creating the information takes a moment...")
err := buildDatabaseDVR()
if err != nil {
ShowError(err, 0)
return
}
fmt.Print("Creating the information takes a moment...")
err := buildDatabaseDVR()
if err != nil {
ShowError(err, 0)
return
}
buildXEPG(false)
buildXEPG(false)
fmt.Println("OK")
println()
fmt.Println("OK")
println()
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
println()
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
println()
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
for i, ipv4 := range System.IPAddressesV4 {
for i, ipv4 := range System.IPAddressesV4 {
switch count := i; {
switch count := i; {
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
}
}
}
}
println()
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
println()
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
for i, ipv4 := range System.IPAddressesV6 {
for i, ipv4 := range System.IPAddressesV6 {
switch count := i; {
switch count := i; {
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 10:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
case count < 100:
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
break
}
}
}
}
println("---")
println("---")
fmt.Println("Settings [General]")
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
fmt.Println("Settings [General]")
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
println("---")
println("---")
fmt.Println("Settings [Files]")
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
fmt.Println("Settings [Files]")
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
fmt.Println(fmt.Sprintf("Missing EPG Mode: %s", Settings.XepgMissingEPGMode))
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
println("---")
println("---")
fmt.Println("Settings [Streaming]")
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
fmt.Println("Settings [Streaming]")
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
println("---")
println("---")
fmt.Println("Settings [Backup]")
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
fmt.Println("Settings [Backup]")
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
}

View File

@@ -1,21 +1,22 @@
package authentication
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sync"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"time"
//"fmt"
//"log"
"time"
//"fmt"
//"log"
)
const tokenLength = 40
@@ -27,19 +28,20 @@ var database string
var databaseFile = "authentication.json"
var data = make(map[string]interface{})
var tokens = make(map[string]interface{})
var data = make(map[string]any)
var tokens = make(map[string]any)
var tokensMu sync.RWMutex
var initAuthentication = false
// Cookie : cookie
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
}
// Framework examples
@@ -128,465 +130,475 @@ func main() {
// Init : databasePath = Path to authentication.json
func Init(databasePath string, validity int) (err error) {
database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile
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{})
// Check if the database already exists
if _, err = os.Stat(database); os.IsNotExist(err) {
// Create an empty database
var defaults = make(map[string]any)
defaults["dbVersion"] = "1.0"
defaults["hash"] = "sha256"
defaults["users"] = make(map[string]any)
if saveDatabase(defaults) != nil {
return
}
}
if saveDatabase(defaults) != nil {
return
}
}
// Loading the database
err = loadDatabase()
// Loading the database
err = loadDatabase()
// Set Token Validity
tokenValidity = validity
initAuthentication = true
return
// 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
}
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 users = data["users"].(map[string]any)
// 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)
var defaults = defaultsForNewUser(username, password)
users[defaults["_id"].(string)] = defaults
saveDatabase(data)
return
return
}
// CreateNewUser : create new user
func CreateNewUser(username, password string) (userID string, err error) {
err = checkInit()
if err != nil {
return
}
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)
var checkIfTheUserAlreadyExists = func(username string, userData map[string]any) (err error) {
var salt = userData["_salt"].(string)
var loginUsername = userData["_username"].(string)
if SHA256(username, salt) == loginUsername {
err = createError(020)
}
if SHA256(username, salt) == loginUsername {
err = createError(020)
}
return
}
return
}
var users = data["users"].(map[string]interface{})
for _, userData := range users {
err = checkIfTheUserAlreadyExists(username, userData.(map[string]interface{}))
if err != nil {
return
}
}
var users = data["users"].(map[string]any)
for _, userData := range users {
err = checkIfTheUserAlreadyExists(username, userData.(map[string]any))
if err != nil {
return
}
}
var defaults = defaultsForNewUser(username, password)
userID = defaults["_id"].(string)
users[userID] = defaults
var defaults = defaultsForNewUser(username, password)
userID = defaults["_id"].(string)
users[userID] = defaults
saveDatabase(data)
saveDatabase(data)
return
return
}
// UserAuthentication : user authentication
func UserAuthentication(username, password string) (token string, err error) {
err = checkInit()
if err != nil {
return
}
err = checkInit()
if err != nil {
return
}
var login = func(username, password string, loginData map[string]interface{}) (err error) {
err = createError(010)
var login = func(username, password string, loginData map[string]any) (err error) {
err = createError(010)
var salt = loginData["_salt"].(string)
var loginUsername = loginData["_username"].(string)
var loginPassword = loginData["_password"].(string)
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
}
}
if SHA256(username, salt) == loginUsername {
if SHA256(password, salt) == loginPassword {
err = nil
}
}
return
}
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
}
}
var users = data["users"].(map[string]any)
for id, loginData := range users {
err = login(username, password, loginData.(map[string]any))
if err == nil {
token = setToken(id, "-")
return
}
}
return
return
}
// CheckTheValidityOfTheToken : check token
func CheckTheValidityOfTheToken(token string) (newToken string, err error) {
err = checkInit()
if err != nil {
return
}
err = checkInit()
if err != nil {
return
}
err = createError(011)
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)
tokensMu.Lock()
defer tokensMu.Unlock()
if expires.Sub(time.Now().Local()) < 0 {
return
}
if v, ok := tokens[token]; ok {
expires := v.(map[string]any)["expires"].(time.Time)
newToken = setToken(userID, token)
if expires.Sub(time.Now().Local()) < 0 {
delete(tokens, token)
return
}
err = nil
// Keep a stable token per session and only refresh expiration.
v.(map[string]any)["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
newToken = token
err = nil
}
} else {
return
}
return
return
}
// GetUserID : get user ID
func GetUserID(token string) (userID string, err error) {
err = checkInit()
if err != nil {
return
}
err = checkInit()
if err != nil {
return
}
err = createError(002)
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)
tokensMu.Lock()
defer tokensMu.Unlock()
if expires.Sub(time.Now().Local()) < 0 {
return
}
if v, ok := tokens[token]; ok {
expires := v.(map[string]any)["expires"].(time.Time)
userID = v.(map[string]any)["id"].(string)
err = nil
}
if expires.Sub(time.Now().Local()) < 0 {
delete(tokens, token)
return
}
return
err = nil
}
return
}
// WriteUserData : save user date
func WriteUserData(userID string, userData map[string]interface{}) (err error) {
func WriteUserData(userID string, userData map[string]any) (err error) {
err = checkInit()
if err != nil {
return
}
err = checkInit()
if err != nil {
return
}
err = createError(030)
err = createError(030)
if v, ok := data["users"].(map[string]interface{})[userID].(map[string]interface{}); ok {
if v, ok := data["users"].(map[string]any)[userID].(map[string]any); ok {
v["data"] = userData
err = saveDatabase(data)
v["data"] = userData
err = saveDatabase(data)
} else {
return
}
} else {
return
}
return
return
}
// ReadUserData : load user date
func ReadUserData(userID string) (userData map[string]interface{}, err error) {
func ReadUserData(userID string) (userData map[string]any, err error) {
err = checkInit()
if err != nil {
return
}
err = checkInit()
if err != nil {
return
}
err = createError(031)
err = createError(031)
if v, ok := data["users"].(map[string]interface{})[userID].(map[string]interface{}); ok {
userData = v["data"].(map[string]interface{})
err = nil
if v, ok := data["users"].(map[string]any)[userID].(map[string]any); ok {
userData = v["data"].(map[string]any)
err = nil
return
}
return
}
return
return
}
// RemoveUser : remove user
func RemoveUser(userID string) (err error) {
err = checkInit()
if err != nil {
return
}
err = checkInit()
if err != nil {
return
}
err = createError(032)
err = createError(032)
if _, ok := data["users"].(map[string]interface{})[userID]; ok {
if _, ok := data["users"].(map[string]any)[userID]; ok {
delete(data["users"].(map[string]interface{}), userID)
err = saveDatabase(data)
delete(data["users"].(map[string]any), userID)
err = saveDatabase(data)
return
}
return
}
return
return
}
// SetDefaultUserData : set default user data
func SetDefaultUserData(defaults map[string]interface{}) (err error) {
func SetDefaultUserData(defaults map[string]any) (err error) {
allUserData, err := GetAllUserData()
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 _, d := range allUserData {
var data = d.(map[string]any)["data"].(map[string]any)
var userID = d.(map[string]any)["_id"].(string)
for k, v := range defaults {
if _, ok := data[k]; ok {
// Key exist
} else {
data[k] = v
}
}
err = WriteUserData(userID, data)
}
return
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 = checkInit()
if err != nil {
return
}
err = createError(032)
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 userData, ok := data["users"].(map[string]any)[userID]; ok {
//var userData = tmp.(map[string]interface{})
var salt = userData.(map[string]any)["_salt"].(string)
if len(username) > 0 {
userData.(map[string]interface{})["_username"] = SHA256(username, salt)
}
if len(username) > 0 {
userData.(map[string]any)["_username"] = SHA256(username, salt)
}
if len(password) > 0 {
userData.(map[string]interface{})["_password"] = SHA256(password, salt)
}
if len(password) > 0 {
userData.(map[string]any)["_password"] = SHA256(password, salt)
}
err = saveDatabase(data)
}
err = saveDatabase(data)
}
return
return
}
// GetAllUserData : get all user data
func GetAllUserData() (allUserData map[string]interface{}, err error) {
func GetAllUserData() (allUserData map[string]any, err error) {
err = checkInit()
if err != nil {
return
}
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
}
if len(data) == 0 {
var defaults = make(map[string]any)
defaults["dbVersion"] = "1.0"
defaults["hash"] = "sha256"
defaults["users"] = make(map[string]any)
saveDatabase(defaults)
data = defaults
}
allUserData = data["users"].(map[string]interface{})
return
allUserData = data["users"].(map[string]any)
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
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)
}
if initAuthentication == false {
err = createError(000)
}
return
return
}
func saveDatabase(tmpMap interface{}) (err error) {
func saveDatabase(tmpMap any) (err error) {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return
}
if err != nil {
return
}
err = ioutil.WriteFile(database, []byte(jsonString), 0600)
if err != nil {
return
}
err = ioutil.WriteFile(database, []byte(jsonString), 0600)
if err != nil {
return
}
return
return
}
func loadDatabase() (err error) {
jsonString, err := ioutil.ReadFile(database)
if err != nil {
return
}
jsonString, err := ioutil.ReadFile(database)
if err != nil {
return
}
err = json.Unmarshal([]byte(jsonString), &data)
if err != nil {
return
}
err = json.Unmarshal([]byte(jsonString), &data)
if err != nil {
return
}
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))
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_"
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)
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"
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)
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"
}
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
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{})
func defaultsForNewUser(username, password string) map[string]any {
var defaults = make(map[string]any)
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]any)
return defaults
return defaults
}
func setToken(id, oldToken string) (newToken string) {
delete(tokens, oldToken)
tokensMu.Lock()
defer tokensMu.Unlock()
if oldToken != "-" {
delete(tokens, oldToken)
}
loopToken:
newToken = randomString(tokenLength)
if _, ok := tokens[newToken]; ok {
goto 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))
var tmp = make(map[string]any)
tmp["id"] = id
tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
tokens[newToken] = tmp
tokens[newToken] = tmp
return
return
}
func mapToJSON(tmpMap interface{}) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
return "{}"
}
return string(jsonString)
func mapToJSON(tmpMap any) 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
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

@@ -1,84 +1,84 @@
package m3u
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"testing"
"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"`
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
}
var file = "test_list_1.m3u"
var content, err = ioutil.ReadFile(file)
if err != nil {
t.Error(err)
return
}
streams, err := MakeInterfaceFromM3U(content)
streams, err := MakeInterfaceFromM3U(content)
if err != nil {
t.Error(err)
}
if err != nil {
t.Error(err)
}
err = checkStream(streams)
if err != nil {
t.Error(err)
}
err = checkStream(streams)
if err != nil {
t.Error(err)
}
fmt.Println("Streams:", len(streams))
t.Log(streams)
fmt.Println("Streams:", len(streams))
t.Log(streams)
}
func checkStream(streamInterface []interface{}) (err error) {
func checkStream(streamInterface []any) (err error) {
for i, s := range streamInterface {
for i, s := range streamInterface {
var stream = s.(map[string]string)
var m3uStream M3UStream
var stream = s.(map[string]string)
var m3uStream M3UStream
jsonString, err := json.MarshalIndent(stream, "", " ")
jsonString, err := json.MarshalIndent(stream, "", " ")
if err == nil {
if err == nil {
err = json.Unmarshal(jsonString, &m3uStream)
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))
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"))
}
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) - - - - -"))
}
log.Println(fmt.Sprintf("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -"))
}
return
return
}

View File

@@ -1,186 +1,186 @@
package m3u
import (
"errors"
"fmt"
"log"
"net/url"
"regexp"
"strings"
"errors"
"fmt"
"log"
"net/url"
"regexp"
"strings"
)
// MakeInterfaceFromM3U :
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []interface{}, err error) {
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []any, err error) {
var content = string(byteStream)
var channelName string
var uuids []string
var content = string(byteStream)
var channelName string
var uuids []string
var parseMetaData = func(channel string) (stream map[string]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]*)`
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")
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-- {
// 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[i]) == 0 || lines[i][0:1] == "#" {
lines = append(lines[:i], lines[i+1:]...)
}
}
}
if len(lines) >= 2 {
if len(lines) >= 2 {
for _, line := range lines {
for _, line := range lines {
_, err := url.ParseRequestURI(line)
_, err := url.ParseRequestURI(line)
switch err {
switch err {
case nil:
stream["url"] = strings.Trim(line, "\r\n")
case nil:
stream["url"] = strings.Trim(line, "\r\n")
default:
default:
var value string
// Alle Parameter parsen
var p = regexp.MustCompile(exceptForParameter)
var streamParameter = p.FindAllString(line, -1)
var value string
// Alle Parameter parsen
var p = regexp.MustCompile(exceptForParameter)
var streamParameter = p.FindAllString(line, -1)
for _, p := range streamParameter {
for _, p := range streamParameter {
line = strings.Replace(line, p, "", 1)
line = strings.Replace(line, p, "", 1)
p = strings.Replace(p, `"`, "", -1)
var parameter = strings.SplitN(p, "=", 2)
p = strings.Replace(p, `"`, "", -1)
var parameter = strings.SplitN(p, "=", 2)
if len(parameter) == 2 {
if len(parameter) == 2 {
// TVG Key als Kleinbuchstaben speichern
switch strings.Contains(parameter[0], "tvg") {
// 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]
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] + " "
}
// 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)
// 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(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 len(channelName) == 0 {
if v, ok := stream["tvg-name"]; ok {
channelName = v
}
if v, ok := stream["tvg-name"]; ok {
channelName = v
}
}
}
channelName = strings.TrimRight(channelName, " ")
channelName = strings.TrimRight(channelName, " ")
// Kanäle ohne Namen werden augelassen
if len(channelName) == 0 {
return
}
// Kanäle ohne Namen werden augelassen
if len(channelName) == 0 {
return
}
stream["name"] = channelName
value = value + channelName
stream["name"] = channelName
value = value + channelName
stream["_values"] = value
stream["_values"] = value
}
}
}
}
}
}
// Nach eindeutiger ID im Stream suchen
for key, value := range stream {
// 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), "tvg-id") {
if strings.Contains(strings.ToLower(key), "id") {
if strings.Contains(strings.ToLower(key), "id") {
if indexOfString(value, uuids) != -1 {
log.Println(fmt.Sprintf("Channel: %s - %s = %s ", stream["name"], key, value))
break
}
if indexOfString(value, uuids) != -1 {
log.Println(fmt.Sprintf("Channel: %s - %s = %s ", stream["name"], key, value))
break
}
uuids = append(uuids, value)
uuids = append(uuids, value)
stream["_uuid.key"] = key
stream["_uuid.value"] = value
break
stream["_uuid.key"] = key
stream["_uuid.value"] = value
break
}
}
}
}
}
}
return
}
return
}
//fmt.Println(content)
if strings.Contains(content, "#EXT-X-TARGETDURATION") || strings.Contains(content, "#EXT-X-MEDIA-SEQUENCE") {
err = errors.New("Invalid M3U file, an extended M3U file is required.")
return
}
//fmt.Println(content)
if strings.Contains(content, "#EXT-X-TARGETDURATION") || strings.Contains(content, "#EXT-X-MEDIA-SEQUENCE") {
err = errors.New("Invalid M3U file, an extended M3U file is required.")
return
}
if strings.Contains(content, "#EXTM3U") {
if strings.Contains(content, "#EXTM3U") {
var channels = strings.Split(content, "#EXTINF")
var channels = strings.Split(content, "#EXTINF")
channels = append(channels[:0], channels[1:]...)
channels = append(channels[:0], channels[1:]...)
for _, channel := range channels {
for _, channel := range channels {
var stream = parseMetaData(channel)
var stream = parseMetaData(channel)
if len(stream) > 0 && stream != nil {
allChannels = append(allChannels, stream)
}
if len(stream) > 0 && stream != nil {
allChannels = append(allChannels, stream)
}
}
}
} else {
} else {
err = errors.New("Invalid M3U file, an extended M3U file is required.")
err = errors.New("Invalid M3U file, an extended M3U file is required.")
}
}
return
return
}
func indexOfString(element string, data []string) int {
for k, v := range data {
if element == v {
return k
}
}
for k, v := range data {
if element == v {
return k
}
}
return -1
return -1
}

View File

@@ -22,10 +22,10 @@ type ClientInfo struct {
OS string `json:"os,required"`
URL string `json:"url,required"`
Response ServerResponse `json:"response,omitempty"`
Response ServerResponse `json:"response"`
}
//ServerResponse : Response from server after client request
// ServerResponse : Response from server after client request
type ServerResponse struct {
Status bool `json:"status,omitempty"`
Reason string `json:"reason,omitempty"`
@@ -106,7 +106,7 @@ func serverRequest() (err error) {
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))
err = fmt.Errorf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), Updater.URL)
return err
}

View File

@@ -13,7 +13,7 @@ import (
)
// Playlisten parsen
func parsePlaylist(filename, fileType string) (channels []interface{}, err error) {
func parsePlaylist(filename, fileType string) (channels []any, err error) {
content, err := readByteFromFile(filename)
var id = strings.TrimSuffix(getFilenameFromPath(filename), path.Ext(getFilenameFromPath(filename)))
@@ -34,7 +34,7 @@ func parsePlaylist(filename, fileType string) (channels []interface{}, err error
}
// Streams filtern
func filterThisStream(s interface{}) (status bool) {
func filterThisStream(s any) (status bool) {
status = false
var stream = s.(map[string]string)

363
src/plex_api.go Normal file
View File

@@ -0,0 +1,363 @@
package src
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
var plexRefreshState = struct {
sync.Mutex
lastRun time.Time
scheduled bool
inProgress bool
queued bool
reason string
missingConfigLogged bool
}{}
// queuePlexGuideRefresh schedules a debounced Plex DVR guide refresh.
func queuePlexGuideRefresh(reason string) {
if Settings.UsePlexAPI == false {
return
}
plexRefreshState.Lock()
if len(strings.TrimSpace(reason)) > 0 {
plexRefreshState.reason = reason
}
if plexRefreshState.scheduled == true || plexRefreshState.inProgress == true {
plexRefreshState.queued = true
plexRefreshState.Unlock()
return
}
plexRefreshState.scheduled = true
delay := plexRefreshDelayLocked()
plexRefreshState.Unlock()
go runPlexGuideRefresh(delay)
}
func runPlexGuideRefresh(initialDelay time.Duration) {
if initialDelay > 0 {
time.Sleep(initialDelay)
}
for {
plexRefreshState.Lock()
reason := strings.TrimSpace(plexRefreshState.reason)
if len(reason) == 0 {
reason = "update"
}
plexRefreshState.scheduled = false
plexRefreshState.inProgress = true
plexRefreshState.queued = false
plexRefreshState.Unlock()
err := refreshPlexGuide(reason)
if err != nil {
ShowError(err, 0)
}
plexRefreshState.Lock()
plexRefreshState.lastRun = time.Now()
runAgain := plexRefreshState.queued
plexRefreshState.inProgress = false
nextDelay := time.Duration(0)
if runAgain == true && Settings.UsePlexAPI == true {
plexRefreshState.scheduled = true
nextDelay = plexRefreshDelayLocked()
}
plexRefreshState.Unlock()
if runAgain == false || Settings.UsePlexAPI == false {
return
}
time.Sleep(nextDelay)
}
}
func plexRefreshDelayLocked() (delay time.Duration) {
const minDelay = 2 * time.Second
const cooldown = 20 * time.Second
if plexRefreshState.lastRun.IsZero() == true {
return minDelay
}
since := time.Since(plexRefreshState.lastRun)
if since >= cooldown {
return minDelay
}
return cooldown - since
}
func refreshPlexGuide(reason string) (err error) {
baseURL, token, ready := getPlexConfig()
if ready == false {
return nil
}
dvrs, err := discoverPlexDVRs(baseURL, token)
if err != nil {
return
}
var refreshed int
var failed int
for _, dvr := range dvrs {
endpoints := getPlexReloadEndpoints(dvr)
if len(endpoints) == 0 {
failed++
continue
}
var endpointErr error
for _, endpoint := range endpoints {
status, _, requestErr := doPlexRequest("POST", baseURL, endpoint, token)
if requestErr != nil {
endpointErr = requestErr
continue
}
if status >= 200 && status < 300 {
refreshed++
endpointErr = nil
break
}
endpointErr = fmt.Errorf("Plex API returned HTTP %d for %s", status, endpoint)
}
if endpointErr != nil {
failed++
}
}
if refreshed == 0 {
if failed == 0 {
return errors.New("Plex API guide refresh failed")
}
return fmt.Errorf("Plex API guide refresh failed for %d DVR(s)", failed)
}
showInfo(fmt.Sprintf("Plex API:Guide reload requested for %d DVR(s) (%s)", refreshed, reason))
if failed > 0 {
showInfo(fmt.Sprintf("Plex API:Guide reload failed for %d DVR(s)", failed))
}
return nil
}
func getPlexConfig() (baseURL, token string, ready bool) {
baseURL = strings.TrimSpace(Settings.PlexURL)
token = strings.TrimSpace(Settings.PlexToken)
if strings.HasPrefix(baseURL, "http://") == false && strings.HasPrefix(baseURL, "https://") == false && len(baseURL) > 0 {
baseURL = "http://" + baseURL
}
baseURL = strings.TrimRight(baseURL, "/")
plexRefreshState.Lock()
defer plexRefreshState.Unlock()
if len(baseURL) == 0 || len(token) == 0 {
if plexRefreshState.missingConfigLogged == false {
showInfo("Plex API:Skipped refresh because plex.url or plex.token is empty")
plexRefreshState.missingConfigLogged = true
}
return "", "", false
}
plexRefreshState.missingConfigLogged = false
return baseURL, token, true
}
func discoverPlexDVRs(baseURL, token string) (dvrs []map[string]any, err error) {
status, body, err := doPlexRequest("GET", baseURL, "/livetv/dvrs", token)
if err != nil {
return
}
if status < 200 || status >= 300 {
err = fmt.Errorf("Plex API returned HTTP %d for /livetv/dvrs", status)
return
}
var payload = make(map[string]any)
err = json.Unmarshal(body, &payload)
if err != nil {
return
}
mediaContainer, ok := payload["MediaContainer"].(map[string]any)
if ok == false {
err = errors.New("Plex API response missing MediaContainer")
return
}
for _, key := range []string{"Dvr", "DVR", "Directory", "Metadata"} {
if raw, found := mediaContainer[key]; found == true {
if list, ok := raw.([]any); ok == true {
dvrs = convertToPlexMapSlice(list)
if len(dvrs) > 0 {
return
}
}
}
}
err = errors.New("Plex API returned no DVR entries")
return
}
func convertToPlexMapSlice(list []any) (dvrs []map[string]any) {
dvrs = make([]map[string]any, 0, len(list))
for _, item := range list {
if dvr, ok := item.(map[string]any); ok == true {
dvrs = append(dvrs, dvr)
}
}
return
}
func getPlexReloadEndpoints(dvr map[string]any) (endpoints []string) {
endpoints = make([]string, 0, 6)
added := make(map[string]bool)
add := func(endpoint string) {
endpoint = strings.TrimSpace(endpoint)
if len(endpoint) == 0 {
return
}
if strings.HasPrefix(endpoint, "/") == false {
endpoint = "/" + endpoint
}
if added[endpoint] == true {
return
}
added[endpoint] = true
endpoints = append(endpoints, endpoint)
}
if key := getPlexMapString(dvr, "key"); len(key) > 0 {
if strings.HasPrefix(key, "/") == true {
add(strings.TrimRight(key, "/") + "/reloadGuide")
} else {
add("/livetv/dvrs/" + url.PathEscape(key) + "/reloadGuide")
}
}
for _, field := range []string{"uuid", "identifier", "machineIdentifier", "clientIdentifier", "id"} {
if value := getPlexMapString(dvr, field); len(value) > 0 {
add("/livetv/dvrs/" + url.PathEscape(value) + "/reloadGuide")
}
}
return
}
func getPlexMapString(data map[string]any, field string) string {
for key, value := range data {
if strings.EqualFold(key, field) == false {
continue
}
switch v := value.(type) {
case string:
return strings.TrimSpace(v)
case float64:
return strconv.FormatInt(int64(v), 10)
}
}
return ""
}
func doPlexRequest(method, baseURL, endpoint, token string) (status int, body []byte, err error) {
requestURL := strings.TrimRight(baseURL, "/") + "/" + strings.TrimLeft(endpoint, "/")
req, err := http.NewRequest(method, requestURL, nil)
if err != nil {
return
}
query := req.URL.Query()
if len(token) > 0 {
query.Set("X-Plex-Token", token)
req.URL.RawQuery = query.Encode()
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Plex-Token", token)
if len(System.DeviceID) > 0 {
req.Header.Set("X-Plex-Client-Identifier", System.DeviceID)
}
if len(System.Name) > 0 {
req.Header.Set("X-Plex-Product", System.Name)
req.Header.Set("X-Plex-Device-Name", System.Name)
}
if len(System.Version) > 0 {
req.Header.Set("X-Plex-Version", System.Version)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
status = resp.StatusCode
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
return
}
return
}

View File

@@ -17,13 +17,13 @@ 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 dataMap = make(map[string]any)
var saveDateFromProvider = func(fileSource, serverFileName, id string, body []byte) (err error) {
var data = make(map[string]interface{})
var data = make(map[string]any)
if value, ok := dataMap[id].(map[string]interface{}); ok {
if value, ok := dataMap[id].(map[string]any); ok {
data = value
} else {
data["id.provider"] = id
@@ -65,7 +65,7 @@ func getProviderData(fileType, fileID string) (err error) {
}
case "compatibility":
data[key] = make(map[string]interface{})
data[key] = make(map[string]any)
case "counter.download":
data[key] = 0.0
@@ -142,7 +142,7 @@ func getProviderData(fileType, fileID string) (err error) {
for dataID, d := range dataMap {
var data = d.(map[string]interface{})
var data = d.(map[string]any)
var fileSource = data["file.source"].(string)
newProvider = false
@@ -220,8 +220,8 @@ func getProviderData(fileType, fileID string) (err error) {
}
// Fehler Counter um 1 erhöhen
var data = make(map[string]interface{})
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
var data = make(map[string]any)
if value, ok := dataMap[dataID].(map[string]any); ok {
data = value
data["counter.error"] = data["counter.error"].(float64) + 1
@@ -238,9 +238,9 @@ func getProviderData(fileType, fileID string) (err error) {
// Berechnen der Fehlerquote
if newProvider == false {
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
if value, ok := dataMap[dataID].(map[string]any); ok {
var data = make(map[string]interface{})
var data = make(map[string]any)
data = value
if data["counter.error"].(float64) == 0 {
@@ -282,15 +282,23 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e
return
}
resp, err := http.Get(providerURL)
req, err := http.NewRequest(http.MethodGet, providerURL, nil)
if err != nil {
return
}
resp.Header.Set("User-Agent", Settings.UserAgent)
req.Header.Set("User-Agent", getUserAgent())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf(fmt.Sprintf("%d: %s "+http.StatusText(resp.StatusCode), resp.StatusCode, providerURL))
err = fmt.Errorf("%d: %s %s", resp.StatusCode, providerURL, http.StatusText(resp.StatusCode))
return
}

View File

@@ -323,7 +323,7 @@ func getErrMsg(errCode int) (errMsg string) {
// 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.")
errMsg = "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")

View File

@@ -47,11 +47,7 @@ type LineupStatus struct {
}
// Lineup : HDHR Lineup /lineup.json
type Lineup []interface {
//GuideName string `json:"GuideName"`
//GuideNumber string `json:"GuideNumber"`
//URL string `json:"URL"`
}
type Lineup []any
// LineupStream : HDHR einzelner Stream im Lineup
type LineupStream struct {

View File

@@ -149,18 +149,18 @@ type DataStruct struct {
}
Streams struct {
Active []interface{}
All []interface{}
Inactive []interface{}
Active []any
All []any
Inactive []any
}
XMLTV struct {
Files []string
Mapping map[string]interface{}
Mapping map[string]any
}
XEPG struct {
Channels map[string]interface{}
Channels map[string]any
XEPGCount int64
}
}
@@ -254,6 +254,9 @@ type Notification struct {
// SettingsStruct : Inhalt der settings.json
type SettingsStruct struct {
API bool `json:"api"`
UsePlexAPI bool `json:"use_plexAPI"`
PlexURL string `json:"plex.url"`
PlexToken string `json:"plex.token"`
AuthenticationAPI bool `json:"authentication.api"`
AuthenticationM3U bool `json:"authentication.m3u"`
AuthenticationPMS bool `json:"authentication.pms"`
@@ -275,30 +278,32 @@ type SettingsStruct struct {
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"`
HDHR map[string]any `json:"hdhr"`
M3U map[string]any `json:"m3u"`
XMLTV map[string]any `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"`
UDPxy string `json:"udpxy"`
Version string `json:"version"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
FilesUpdate bool `json:"files.update"`
Filter map[int64]any `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"`
UDPxy string `json:"udpxy"`
Version string `json:"version"`
XepgMissingEPGMode string `json:"xepg.missing.epg.mode"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
WizardCompleted bool `json:"wizard.completed"`
}
// LanguageUI : Sprache für das WebUI

View File

@@ -6,11 +6,11 @@ type RequestStruct struct {
Cmd string `json:"cmd,required"`
// Benutzer
DeleteUser bool `json:"deleteUser,omitempty"`
UserData map[string]interface{} `json:"userData,omitempty"`
DeleteUser bool `json:"deleteUser,omitempty"`
UserData map[string]any `json:"userData,omitempty"`
// Mapping
EpgMapping map[string]interface{} `json:"epgMapping,omitempty"`
EpgMapping map[string]any `json:"epgMapping,omitempty"`
// Restore
Base64 string `json:"base64,omitempty"`
@@ -18,6 +18,9 @@ type RequestStruct struct {
// Neue Werte für die Einstellungen (settings.json)
Settings struct {
API *bool `json:"api,omitempty"`
UsePlexAPI *bool `json:"use_plexAPI,omitempty"`
PlexURL *string `json:"plex.url,omitempty"`
PlexToken *string `json:"plex.token,omitempty"`
AuthenticationAPI *bool `json:"authentication.api,omitempty"`
AuthenticationM3U *bool `json:"authentication.m3u,omitempty"`
AuthenticationPMS *bool `json:"authentication.pms,omitempty"`
@@ -26,7 +29,7 @@ type RequestStruct struct {
BackupKeep *int `json:"backup.keep,omitempty"`
BackupPath *string `json:"backup.path,omitempty"`
Buffer *string `json:"buffer,omitempty"`
BufferSize *int `json:"buffer.size.kb, 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"`
@@ -40,24 +43,25 @@ type RequestStruct struct {
UDPxy *string `json:"udpxy,omitempty"`
Update *[]string `json:"update,omitempty"`
UserAgent *string `json:"user.agent,omitempty"`
XepgMissingEPGMode *string `json:"xepg.missing.epg.mode,omitempty"`
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
XteveAutoUpdate *bool `json:"xteveAutoUpdate,omitempty"`
SchemeM3U *string `json:"scheme.m3u,omitempty"`
SchemeXML *string `json:"scheme.xml,omitempty"`
} `json:"settings,omitempty"`
} `json:"settings"`
// Upload Logo
Filename string `json:"filename,omitempty"`
// Filter
Filter map[int64]interface{} `json:"filter,omitempty"`
Filter map[int64]any `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"`
HDHR map[string]any `json:"hdhr,omitempty"`
M3U map[string]any `json:"m3u,omitempty"`
XMLTV map[string]any `json:"xmltv,omitempty"`
} `json:"files"`
// Wizard
Wizard struct {
@@ -65,7 +69,7 @@ type RequestStruct struct {
M3U *string `json:"m3u,omitempty"`
Tuner *int `json:"tuner,omitempty"`
XMLTV *string `json:"xmltv,omitempty"`
} `json:"wizard,omitempty"`
} `json:"wizard"`
}
// ResponseStruct : Antworten an den Client (WEB)
@@ -84,7 +88,7 @@ type ResponseStruct struct {
Warnings int `json:"warnings"`
XEPGCount int64 `json:"xepg"`
XML string `json:"xepg-url,required"`
} `json:"clientInfo,omitempty"`
} `json:"clientInfo"`
Data struct {
Playlist struct {
@@ -102,20 +106,20 @@ type ResponseStruct struct {
}
} `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 SettingsStruct `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"`
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 SettingsStruct `json:"settings,required"`
Status bool `json:"status,required"`
Token string `json:"token,omitempty"`
Users map[string]any `json:"users,omitempty"`
Wizard int `json:"wizard,omitempty"`
XEPG map[string]any `json:"xepg,required"`
Notification map[string]Notification `json:"notification,omitempty"`
}

View File

@@ -59,7 +59,7 @@ func createSystemFiles() (err error) {
err = checkFile(filename)
if err != nil {
// Datei existiert nicht, wird jetzt erstellt
err = saveMapToJSONFile(filename, make(map[string]interface{}))
err = saveMapToJSONFile(filename, make(map[string]any))
if err != nil {
return
}
@@ -97,15 +97,28 @@ func loadSettings() (settings SettingsStruct, err error) {
return
}
// Deafult Werte setzten
var defaults = make(map[string]interface{})
var dataMap = make(map[string]interface{})
var freshInstall = len(settingsMap) == 0
dataMap["xmltv"] = make(map[string]interface{})
dataMap["m3u"] = make(map[string]interface{})
dataMap["hdhr"] = make(map[string]interface{})
// Deafult Werte setzten
var defaults = make(map[string]any)
var dataMap = make(map[string]any)
dataMap["xmltv"] = make(map[string]any)
dataMap["m3u"] = make(map[string]any)
dataMap["hdhr"] = make(map[string]any)
defaultFFmpegPath := ""
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
containerFFmpegPath := "/usr/local/bin/ffmpeg"
if checkFile(containerFFmpegPath) == nil {
defaultFFmpegPath = containerFFmpegPath
}
}
defaults["api"] = false
defaults["use_plexAPI"] = false
defaults["plex.url"] = ""
defaults["plex.token"] = ""
defaults["authentication.api"] = false
defaults["authentication.m3u"] = false
defaults["authentication.pms"] = false
@@ -118,27 +131,34 @@ func loadSettings() (settings SettingsStruct, err error) {
defaults["buffer.timeout"] = 500
defaults["cache.images"] = false
defaults["epgSource"] = "PMS"
defaults["ffmpeg.path"] = defaultFFmpegPath
defaults["ffmpeg.options"] = System.FFmpeg.DefaultOptions
defaults["vlc.options"] = System.VLC.DefaultOptions
defaults["files"] = dataMap
defaults["files.update"] = true
defaults["filter"] = make(map[string]interface{})
defaults["filter"] = make(map[string]any)
defaults["git.branch"] = System.Branch
defaults["language"] = "en"
defaults["log.entries.ram"] = 500
defaults["mapping.first.channel"] = 1000
defaults["xepg.missing.epg.mode"] = "strict"
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["user.agent"] = defaultUserAgent
defaults["uuid"] = createUUID()
defaults["udpxy"] = ""
defaults["version"] = System.DBVersion
defaults["xteveAutoUpdate"] = true
defaults["temp.path"] = System.Folder.Temp
defaults["wizard.completed"] = !freshInstall
var defaultTempPath = System.Folder.Temp
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
defaultTempPath = System.Folder.Config + "tmp" + string(os.PathSeparator)
}
defaults["temp.path"] = defaultTempPath
// Default Werte setzen
for key, value := range defaults {
@@ -163,7 +183,12 @@ func loadSettings() (settings SettingsStruct, err error) {
}
if len(settings.FFmpegPath) == 0 {
settings.FFmpegPath = searchFileInOS("ffmpeg")
containerFFmpegPath := "/usr/local/bin/ffmpeg"
if len(os.Getenv("XTEVE_CONFIG")) > 0 && checkFile(containerFFmpegPath) == nil {
settings.FFmpegPath = containerFFmpegPath
} else {
settings.FFmpegPath = searchFileInOS("ffmpeg")
}
}
if len(settings.VLCPath) == 0 {
@@ -201,8 +226,36 @@ func saveSettings(settings SettingsStruct) (err error) {
settings.BufferTimeout = 0
}
var userAgent = strings.TrimSpace(settings.UserAgent)
if len(userAgent) == 0 || userAgent == System.Name {
settings.UserAgent = defaultUserAgent
}
settings.XepgMissingEPGMode = strings.ToLower(strings.TrimSpace(settings.XepgMissingEPGMode))
if settings.XepgMissingEPGMode != "relaxed" {
settings.XepgMissingEPGMode = "strict"
}
settings.TempPath = strings.TrimRight(settings.TempPath, string(os.PathSeparator)) + string(os.PathSeparator)
System.Folder.Temp = settings.TempPath + settings.UUID + string(os.PathSeparator)
err = checkFolder(System.Folder.Temp)
if err != nil {
fallbackTempPath := System.Folder.Config + "tmp" + string(os.PathSeparator)
fallbackTempFolder := fallbackTempPath + settings.UUID + string(os.PathSeparator)
fallbackErr := checkFolder(fallbackTempFolder)
if fallbackErr != nil {
return err
}
settings.TempPath = fallbackTempPath
System.Folder.Temp = fallbackTempFolder
showInfo(fmt.Sprintf("Temporary Folder:Fallback to %s", getPlatformPath(System.Folder.Temp)))
}
err = writeByteToFile(System.File.Settings, []byte(mapToJSON(settings)))
if err != nil {
return

View File

@@ -159,7 +159,6 @@ func searchFileInOS(file string) (path string) {
return
}
//
func removeChildItems(dir string) error {
files, err := filepath.Glob(filepath.Join(dir, "*"))
@@ -180,7 +179,7 @@ func removeChildItems(dir string) error {
}
// JSON
func mapToJSON(tmpMap interface{}) string {
func mapToJSON(tmpMap any) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil {
@@ -190,30 +189,30 @@ func mapToJSON(tmpMap interface{}) string {
return string(jsonString)
}
func jsonToMap(content string) map[string]interface{} {
func jsonToMap(content string) map[string]any {
var tmpMap = make(map[string]interface{})
var tmpMap = make(map[string]any)
json.Unmarshal([]byte(content), &tmpMap)
return (tmpMap)
}
func jsonToMapInt64(content string) map[int64]interface{} {
func jsonToMapInt64(content string) map[int64]any {
var tmpMap = make(map[int64]interface{})
var tmpMap = make(map[int64]any)
json.Unmarshal([]byte(content), &tmpMap)
return (tmpMap)
}
func jsonToInterface(content string) (tmpMap interface{}, err error) {
func jsonToInterface(content string) (tmpMap any, err error) {
err = json.Unmarshal([]byte(content), &tmpMap)
return
}
func saveMapToJSONFile(file string, tmpMap interface{}) error {
func saveMapToJSONFile(file string, tmpMap any) error {
var filename = getPlatformFile(file)
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
@@ -230,7 +229,7 @@ func saveMapToJSONFile(file string, tmpMap interface{}) error {
return nil
}
func loadJSONFileToMap(file string) (tmpMap map[string]interface{}, err error) {
func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) {
f, err := os.Open(getPlatformFile(file))
defer f.Close()
@@ -360,7 +359,7 @@ func randomString(n int) string {
return string(bytes)
}
func parseTemplate(content string, tmpMap map[string]interface{}) (result string) {
func parseTemplate(content string, tmpMap map[string]any) (result string) {
t := template.Must(template.New("template").Parse(content))

View File

@@ -48,12 +48,12 @@ func BinaryUpdate() (err error) {
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == 404 {
err = fmt.Errorf(fmt.Sprintf("Update Server: %s (%s)", http.StatusText(resp.StatusCode), gitInfo))
err = fmt.Errorf("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))
err = fmt.Errorf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), gitInfo)
return err
}
@@ -87,7 +87,7 @@ func BinaryUpdate() (err error) {
err = up2date.GetVersion()
if err != nil {
debug = fmt.Sprintf(err.Error())
debug = err.Error()
showDebug(debug, 1)
return nil
@@ -95,7 +95,7 @@ func BinaryUpdate() (err error) {
if len(updater.Response.Reason) > 0 {
err = fmt.Errorf(fmt.Sprintf("Update Server: %s", updater.Response.Reason))
err = fmt.Errorf("Update Server: %s", updater.Response.Reason)
ShowError(err, 6002)
return nil
@@ -192,7 +192,7 @@ checkVersion:
}
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
if oldFilter, ok := settingsMap["filter"].([]interface{}); ok {
if oldFilter, ok := settingsMap["filter"].([]any); ok {
var newFilterMap = convertToNewFilter(oldFilter)
settingsMap["filter"] = newFilterMap
@@ -252,11 +252,11 @@ checkVersion:
return
}
func convertToNewFilter(oldFilter []interface{}) (newFilterMap map[int]interface{}) {
func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) {
newFilterMap = make(map[int]interface{})
newFilterMap = make(map[int]any)
switch reflect.TypeOf(oldFilter).Kind() {
switch reflect.TypeFor[[]any]().Kind() {
case reflect.Slice:
s := reflect.ValueOf(oldFilter)
@@ -285,7 +285,7 @@ func setValueForUUID() (err error) {
for _, c := range xepg {
var xepgChannel = c.(map[string]interface{})
var xepgChannel = c.(map[string]any)
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {

15
src/user_agent.go Normal file
View File

@@ -0,0 +1,15 @@
package src
import "strings"
const defaultUserAgent = "otg/1.5.1 (AppleTv Apple TV 4; tvOS16.0; appletv.client) libcurl/7.58.0 OpenSSL/1.0.2o zlib/1.2.11 clib/1.8.56"
func getUserAgent() string {
var userAgent = strings.TrimSpace(Settings.UserAgent)
if len(userAgent) == 0 {
return defaultUserAgent
}
return userAgent
}

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,14 @@ import (
"github.com/gorilla/websocket"
)
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// StartWebserver : Startet den Webserver
func StartWebserver() (err error) {
@@ -336,7 +344,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
}
*/
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
ShowError(err, 0)
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
@@ -558,7 +566,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
default:
fmt.Println("+ + + + + + + + + + +", request.Cmd)
var requestMap = make(map[string]interface{}) // Debug
var requestMap = make(map[string]any) // Debug
_ = requestMap
if System.Dev == true {
fmt.Println(mapToJSON(requestMap))
@@ -591,7 +599,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
// Web : Web Server /web/
func Web(w http.ResponseWriter, r *http.Request) {
var lang = make(map[string]interface{})
var lang = make(map[string]any)
var err error
var requestFile = strings.Replace(r.URL.Path, "/web", "html", -1)
@@ -629,7 +637,7 @@ func Web(w http.ResponseWriter, r *http.Request) {
if System.ScanInProgress == 0 {
if len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 {
if Settings.WizardCompleted == false && len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 {
System.ConfigurationWizard = true
}
@@ -1028,7 +1036,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
defaults.ClientInfo.XEPGCount = Data.XEPG.XEPGCount
var XEPG = make(map[string]interface{})
var XEPG = make(map[string]any)
if len(Data.Streams.Active) > 0 {
@@ -1037,8 +1045,8 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
} else {
XEPG["epgMapping"] = make(map[string]interface{})
XEPG["xmltvMap"] = make(map[string]interface{})
XEPG["epgMapping"] = make(map[string]any)
XEPG["xmltvMap"] = make(map[string]any)
}

View File

@@ -9,6 +9,7 @@ import (
"path"
"runtime"
"sort"
"unicode"
"crypto/md5"
"encoding/hex"
@@ -68,6 +69,7 @@ func buildXEPG(background bool) {
cleanupXEPG()
createXMLTVFile()
createM3UFile()
queuePlexGuideRefresh("xepg rebuild")
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
@@ -84,6 +86,7 @@ func buildXEPG(background bool) {
createXMLTVFile()
createM3UFile()
queuePlexGuideRefresh("xepg image cache refresh")
System.ImageCachingInProgress = 0
@@ -113,6 +116,7 @@ func buildXEPG(background bool) {
createXMLTVFile()
createM3UFile()
queuePlexGuideRefresh("xepg rebuild")
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 {
@@ -127,6 +131,7 @@ func buildXEPG(background bool) {
createXMLTVFile()
createM3UFile()
queuePlexGuideRefresh("xepg image cache refresh")
System.ImageCachingInProgress = 0
@@ -179,6 +184,7 @@ func updateXEPG(background bool) {
createXMLTVFile()
createM3UFile()
queuePlexGuideRefresh("xepg update")
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0
@@ -207,9 +213,9 @@ func updateXEPG(background bool) {
func createXEPGMapping() {
Data.XMLTV.Files = getLocalProviderFiles("xmltv")
Data.XMLTV.Mapping = make(map[string]interface{})
Data.XMLTV.Mapping = make(map[string]any)
var tmpMap = make(map[string]interface{})
var tmpMap = make(map[string]any)
var friendlyDisplayName = func(channel Channel) (displayName string) {
var dn = channel.DisplayName
@@ -250,10 +256,10 @@ func createXEPGMapping() {
if err == nil {
// Daten aus der XML Datei in eine temporäre Map schreiben
var xmltvMap = make(map[string]interface{})
var xmltvMap = make(map[string]any)
for _, c := range xmltv.Channel {
var channel = make(map[string]interface{})
var channel = make(map[string]any)
channel["id"] = c.ID
channel["display-name"] = friendlyDisplayName(*c)
@@ -271,7 +277,7 @@ func createXEPGMapping() {
}
Data.XMLTV.Mapping = tmpMap
tmpMap = make(map[string]interface{})
tmpMap = make(map[string]any)
} else {
@@ -282,7 +288,7 @@ func createXEPGMapping() {
}
// Auswahl für den Dummy erstellen
var dummy = make(map[string]interface{})
var dummy = make(map[string]any)
var times = []string{"30", "60", "90", "120", "180", "240", "360"}
for _, i := range times {
@@ -306,7 +312,7 @@ func createXEPGDatabase() (err error) {
var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit)
Data.Cache.Streams.Active = make([]string, 0, System.UnfilteredChannelLimit)
Data.XEPG.Channels = make(map[string]interface{}, System.UnfilteredChannelLimit)
Data.XEPG.Channels = make(map[string]any, System.UnfilteredChannelLimit)
Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG)
if err != nil {
@@ -521,10 +527,132 @@ func createXEPGDatabase() (err error) {
return
}
func normalizeXEPGMatchValue(value string) string {
value = strings.TrimSpace(value)
if len(value) == 0 {
return ""
}
return strings.Map(func(r rune) rune {
switch {
case unicode.IsLetter(r):
return unicode.ToLower(r)
case unicode.IsDigit(r):
return r
default:
return -1
}
}, value)
}
func appendXEPGIssueSample(samples map[string]struct{}, value string) {
value = strings.TrimSpace(value)
if len(value) == 0 {
return
}
samples[value] = struct{}{}
}
func xepgIssueSampleText(samples map[string]struct{}, max int) string {
if len(samples) == 0 {
return ""
}
list := make([]string, 0, len(samples))
for name := range samples {
list = append(list, name)
}
sort.Strings(list)
if len(list) > max {
return strings.Join(list[:max], ", ") + ", ..."
}
return strings.Join(list, ", ")
}
func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGChannelStruct) (channelID string, channel map[string]any, ok bool) {
var candidateValues = map[string]struct{}{}
addCandidate := func(value string) {
normalized := normalizeXEPGMatchValue(value)
if len(normalized) == 0 {
return
}
candidateValues[normalized] = struct{}{}
}
addCandidate(xepgChannel.XName)
addCandidate(xepgChannel.Name)
addCandidate(xepgChannel.TvgName)
addCandidate(xepgChannel.TvgID)
if len(candidateValues) == 0 {
return "", nil, false
}
if _, exists := candidateValues[normalizeXEPGMatchValue(xepgChannel.TvgID)]; exists {
if direct, found := xmltvChannels[xepgChannel.TvgID].(map[string]any); found {
return xepgChannel.TvgID, direct, true
}
}
var matches []struct {
id string
channel map[string]any
}
for id, data := range xmltvChannels {
xmltvChannel, castOK := data.(map[string]any)
if castOK == false {
continue
}
if _, match := candidateValues[normalizeXEPGMatchValue(id)]; match {
matches = append(matches, struct {
id string
channel map[string]any
}{id: id, channel: xmltvChannel})
continue
}
displayName, hasDisplayName := xmltvChannel["display-name"].(string)
if hasDisplayName == false {
continue
}
if _, match := candidateValues[normalizeXEPGMatchValue(displayName)]; match {
matches = append(matches, struct {
id string
channel map[string]any
}{id: id, channel: xmltvChannel})
}
}
if len(matches) != 1 {
return "", nil, false
}
return matches[0].id, matches[0].channel, true
}
// Kanäle automatisch zuordnen und das Mapping überprüfen
func mapping() (err error) {
showInfo("XEPG:" + "Map channels")
strictMissingEPGMode := Settings.XepgMissingEPGMode != "relaxed"
missingEPGCount := 0
missingXMLTVCount := 0
autoRemapCount := 0
relaxedKeepCount := 0
relaxedDummyCount := 0
missingEPGSamples := map[string]struct{}{}
missingXMLTVSamples := map[string]struct{}{}
autoRemapSamples := map[string]struct{}{}
relaxedKeepSamples := map[string]struct{}{}
relaxedDummySamples := map[string]struct{}{}
for xepg, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct
@@ -537,7 +665,7 @@ func mapping() (err error) {
if xepgChannel.XActive == false {
// Werte kann "-" sein, deswegen len < 1
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XmltvFile) < 1 {
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XMapping) < 1 {
var tvgID = xepgChannel.TvgID
@@ -549,16 +677,16 @@ func mapping() (err error) {
for file, xmltvChannels := range Data.XMLTV.Mapping {
if channel, ok := xmltvChannels.(map[string]interface{})[tvgID]; ok {
if channel, ok := xmltvChannels.(map[string]any)[tvgID]; ok {
if channelID, ok := channel.(map[string]interface{})["id"].(string); ok {
if channelID, ok := channel.(map[string]any)["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 icon, ok := channel.(map[string]any)["icon"].(string); ok {
if len(icon) > 0 {
xepgChannel.TvgLogo = icon
}
@@ -585,9 +713,9 @@ func mapping() (err error) {
if file != "xTeVe Dummy" {
if value, ok := Data.XMLTV.Mapping[file].(map[string]interface{}); ok {
if value, ok := Data.XMLTV.Mapping[file].(map[string]any); ok {
if channel, ok := value[mapping].(map[string]interface{}); ok {
if channel, ok := value[mapping].(map[string]any); ok {
// Kanallogo aktualisieren
if logo, ok := channel["icon"].(string); ok {
@@ -599,20 +727,68 @@ func mapping() (err error) {
}
} else {
if channelID, replacementChannel, remapOK := findXEPGReplacementChannel(value, xepgChannel); remapOK {
xepgChannel.XMapping = channelID
ShowError(fmt.Errorf(fmt.Sprintf("Missing EPG data: %s", xepgChannel.Name)), 0)
showWarning(2302)
xepgChannel.XActive = false
if logo, ok := replacementChannel["icon"].(string); ok {
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 {
xepgChannel.TvgLogo = logo
}
}
autoRemapCount++
name := strings.TrimSpace(xepgChannel.Name)
if len(name) == 0 {
name = strings.TrimSpace(xepgChannel.XName)
}
if len(name) == 0 {
name = xepg
}
appendXEPGIssueSample(autoRemapSamples, name)
} else {
name := strings.TrimSpace(xepgChannel.Name)
if len(name) == 0 {
name = strings.TrimSpace(xepgChannel.XName)
}
if len(name) == 0 {
name = xepg
}
if strictMissingEPGMode == true {
missingEPGCount++
appendXEPGIssueSample(missingEPGSamples, name)
xepgChannel.XActive = false
} else {
relaxedKeepCount++
appendXEPGIssueSample(relaxedKeepSamples, name)
}
}
}
} else {
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file)))
providerName := getProviderParameter(fileID, "xmltv", "name")
if len(strings.TrimSpace(providerName)) == 0 {
providerName = file
}
ShowError(fmt.Errorf("Missing XMLTV file: %s", getProviderParameter(fileID, "xmltv", "name")), 0)
showWarning(2301)
xepgChannel.XActive = false
if strictMissingEPGMode == true {
missingXMLTVCount++
appendXEPGIssueSample(missingXMLTVSamples, providerName)
xepgChannel.XActive = false
} else {
relaxedDummyCount++
name := strings.TrimSpace(xepgChannel.Name)
if len(name) == 0 {
name = strings.TrimSpace(xepgChannel.XName)
}
if len(name) == 0 {
name = xepg
}
appendXEPGIssueSample(relaxedDummySamples, name)
}
}
@@ -634,6 +810,28 @@ func mapping() (err error) {
}
if autoRemapCount > 0 {
showInfo(fmt.Sprintf("XEPG:%d channel mappings were auto-remapped (examples: %s)", autoRemapCount, xepgIssueSampleText(autoRemapSamples, 8)))
}
if missingEPGCount > 0 {
showWarning(2302)
showInfo(fmt.Sprintf("XEPG:%d channels have missing EPG mappings and were deactivated (examples: %s)", missingEPGCount, xepgIssueSampleText(missingEPGSamples, 8)))
}
if missingXMLTVCount > 0 {
showWarning(2301)
showInfo(fmt.Sprintf("XEPG:%d channels reference missing XMLTV files and were deactivated (sources: %s)", missingXMLTVCount, xepgIssueSampleText(missingXMLTVSamples, 5)))
}
if relaxedKeepCount > 0 {
showInfo(fmt.Sprintf("XEPG:%d channels kept active in relaxed mode despite missing EPG mappings (examples: %s)", relaxedKeepCount, xepgIssueSampleText(relaxedKeepSamples, 8)))
}
if relaxedDummyCount > 0 {
showInfo(fmt.Sprintf("XEPG:%d channels will use xTeVe Dummy guide in relaxed mode because XMLTV sources were unavailable (examples: %s)", relaxedDummyCount, xepgIssueSampleText(relaxedDummySamples, 8)))
}
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
if err != nil {
return
@@ -667,7 +865,7 @@ func createXMLTVFile() (err error) {
}
if len(Data.XMLTV.Files) == 0 && len(Data.Streams.Active) == 0 {
Data.XEPG.Channels = make(map[string]interface{})
Data.XEPG.Channels = make(map[string]any)
return
}
@@ -735,6 +933,13 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile
var channelID = xepgChannel.XMapping
relaxedMissingEPGMode := Settings.XepgMissingEPGMode == "relaxed"
fallbackToDummy := func() {
dummyChannel := xepgChannel
dummyChannel.XmltvFile = "xTeVe Dummy"
dummyChannel.XMapping = "240_Minutes"
xepgXML = createDummyProgram(dummyChannel)
}
var xmltv XMLTV
@@ -744,6 +949,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
err = getLocalXMLTV(xmltvFile, &xmltv)
if err != nil {
if relaxedMissingEPGMode == true {
fallbackToDummy()
err = nil
}
return
}
@@ -817,6 +1026,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
}
if len(xepgXML.Program) == 0 && relaxedMissingEPGMode == true && xmltvFile != System.Folder.Data+"xTeVe Dummy" {
fallbackToDummy()
}
return
}
@@ -830,7 +1043,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
var currentDay = currentTime.Format("20060102")
var startTime, _ = time.Parse("20060102150405", currentDay+"000000")
showInfo("Create Dummy Guide:" + "Time offset" + offset + " - " + xepgChannel.XName)
showDebug("Create Dummy Guide:"+"Time offset"+offset+" - "+xepgChannel.XName, 2)
var dl = strings.Split(xepgChannel.XMapping, "_")
dummyLength, err := strconv.Atoi(dl[0])
@@ -839,7 +1052,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
return
}
for d := 0; d < 4; d++ {
for d := range 4 {
var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24))

View File

@@ -16,6 +16,8 @@ function login() {
if (value.length == 0) {
inputs[i].style.borderColor = "red"
err = true
} else {
inputs[i].style.borderColor = ""
}
data[key] = value
@@ -30,7 +32,6 @@ function login() {
if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) {
alert("sdafsd")
document.getElementById('password').style.borderColor = "red"
document.getElementById('confirm').style.borderColor = "red"

View File

@@ -5,6 +5,8 @@ var SEARCH_MAPPING = new Object()
var UNDO = new Object()
var SERVER_CONNECTION = false
var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
var LAST_BULK_CHECKBOX:HTMLInputElement = null
// Menü
@@ -21,7 +23,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// 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.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
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"))
@@ -51,7 +53,44 @@ function showElement(elmID, type) {
case false: cssClass = "none"; break;
}
document.getElementById(elmID).className = cssClass;
var element = document.getElementById(elmID)
if (element == null) {
return
}
element.className = cssClass;
}
function setConnectionState(state:string, text:string = "") {
var label:string = text
if (label == undefined || label.length == 0) {
switch (state) {
case "online":
label = "Connected"
break
case "busy":
label = "Syncing"
break
case "offline":
label = "Offline"
break
default:
label = "Connecting"
break
}
}
var indicator = document.getElementById("connection-indicator")
if (indicator == null) {
return
}
indicator.className = "status-" + state
indicator.innerText = label
}
function changeButtonAction(element, buttonID, attribute) {
@@ -144,6 +183,59 @@ function getAllSelectedChannels():string[] {
return channels
}
function scheduleChannelRangeSelection(checkbox:HTMLInputElement, event:MouseEvent) {
var shiftPressed = false
if (event != undefined && event.shiftKey == true) {
shiftPressed = true
}
// Run after the native checkbox toggle so we copy the final checked state.
setTimeout(function() {
selectChannelRange(checkbox, shiftPressed)
}, 0)
}
function selectChannelRange(checkbox:HTMLInputElement, shiftPressed:boolean) {
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
return
}
var table = document.getElementById("content_table")
if (table == null) {
return
}
var trs = table.getElementsByTagName("TR")
var visibleCheckboxes:HTMLInputElement[] = new Array()
for (var i = 1; i < trs.length; i++) {
if ((trs[i] as HTMLElement).style.display != "none") {
var bulkCheckbox = (trs[i] as HTMLTableRowElement).querySelector("input.bulk") as HTMLInputElement
if (bulkCheckbox != null) {
visibleCheckboxes.push(bulkCheckbox)
}
}
}
var currentIndex = visibleCheckboxes.indexOf(checkbox)
var previousIndex = -1
if (LAST_BULK_CHECKBOX != null) {
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX)
}
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
var start = Math.min(previousIndex, currentIndex)
var end = Math.max(previousIndex, currentIndex)
for (var i = start; i <= end; i++) {
visibleCheckboxes[i].checked = checkbox.checked
}
}
LAST_BULK_CHECKBOX = checkbox
}
function selectAllChannels() {
var bulk:Boolean = false
@@ -173,6 +265,7 @@ function selectAllChannels() {
}
LAST_BULK_CHECKBOX = null
return
}
@@ -197,6 +290,7 @@ function bulkEdit() {
(rows[i] as HTMLInputElement).checked = false
}
LAST_BULK_CHECKBOX = null
return
}
@@ -379,17 +473,19 @@ function searchInMapping() {
function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")){
var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
var elm = document.getElementById("box-wrapper");
if (elm != null && content != null){
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;
var contentTop = content.getBoundingClientRect().top
var freeSpace = window.innerHeight - contentTop - 26
if (freeSpace < 180) {
freeSpace = 180
}
elm.style.height = window.innerHeight - elementsHeight + "px";
elm.style.height = freeSpace + "px";
}

View File

@@ -29,9 +29,18 @@ function showLogs(bottom:boolean) {
var logs = SERVER["log"]["log"]
var div = document.getElementById("content_log")
var wrapper = document.getElementById("box-wrapper") as HTMLElement
var shouldStickToBottom:boolean = bottom
div.innerHTML = ""
if (wrapper != null && shouldStickToBottom == false) {
var distanceToBottom:number = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight
if (distanceToBottom < 80) {
shouldStickToBottom = true
}
}
var keys = getObjKeys(logs)
keys.forEach(logID => {
@@ -44,9 +53,8 @@ function showLogs(bottom:boolean) {
setTimeout(function(){
if (bottom == true) {
if (shouldStickToBottom == true && wrapper != null) {
var wrapper = document.getElementById("box-wrapper");
wrapper.scrollTop = wrapper.scrollHeight;
}

View File

@@ -37,6 +37,7 @@ class MainMenuItem extends MainMenu {
var item = document.createElement("LI")
item.setAttribute("onclick", "javascript: openThisMenu(this)")
item.setAttribute("id", this.id)
item.setAttribute("data-menu", this.menuKey)
var img = this.createIMG(this.imgSrc)
var value = this.createValue(this.value)
@@ -64,7 +65,7 @@ class MainMenuItem extends MainMenu {
break
case "mapping":
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}"]
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}", "{{.mapping.table.edit}}"]
break
}
@@ -424,11 +425,7 @@ class Content {
cell.child = true
cell.childType = "IMG"
cell.imageURL = data[key]["tvg-logo"]
var td = cell.createCell()
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
tr.appendChild(cell.createCell())
// Kanalname
var cell:Cell = new Cell()
@@ -436,10 +433,7 @@ class Content {
cell.childType = "P"
cell.className = data[key]["x-category"]
cell.value = data[key]["x-name"]
var td = cell.createCell()
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
tr.appendChild(cell.createCell())
// Playlist
@@ -448,10 +442,7 @@ class Content {
cell.childType = "P"
//cell.value = data[key]["_file.m3u.name"]
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name")
var td = cell.createCell()
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
tr.appendChild(cell.createCell())
// Gruppe (group-title)
@@ -459,10 +450,7 @@ class Content {
cell.child = true
cell.childType = "P"
cell.value = data[key]["x-group-title"]
var td = cell.createCell()
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
tr.appendChild(cell.createCell())
// XMLTV Datei
var cell:Cell = new Cell()
@@ -475,10 +463,7 @@ class Content {
cell.value = data[key]["x-xmltv-file"]
}
var td = cell.createCell()
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
tr.appendChild(cell.createCell())
// XMLTV Kanal
var cell:Cell = new Cell()
@@ -490,11 +475,18 @@ class Content {
value = data[key]["x-mapping"].substring(0, 20) + "..."
}
cell.value = value
var td = cell.createCell()
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(cell.createCell())
tr.appendChild(td)
var cell:Cell = new Cell()
cell.child = true
cell.childType = "EDIT"
cell.value = "{{.button.edit}}"
var editTd = cell.createCell()
var editButton = (editTd.firstChild as HTMLInputElement)
editButton.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
editButton.setAttribute("id", key)
editButton.setAttribute("aria-label", "Edit " + data[key]["x-name"])
tr.appendChild(editTd)
rows.push(tr)
});
@@ -561,6 +553,9 @@ class Cell {
(element as HTMLInputElement).checked = this.value;
(element as HTMLInputElement).type = "checkbox";
(element as HTMLInputElement).className = "bulk hideBulk";
(element as HTMLInputElement).addEventListener("click", function(event) {
scheduleChannelRangeSelection((element as HTMLInputElement), (event as MouseEvent))
})
break
case "BULK_HEAD":
@@ -578,6 +573,14 @@ class Cell {
element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''" )
//onerror="this.onerror=null;this.src='missing.gif';"
}
break
case "EDIT":
element = document.createElement("INPUT");
(element as HTMLInputElement).type = "button";
(element as HTMLInputElement).value = this.value;
(element as HTMLInputElement).className = "mapping-edit-button";
break
}
td.appendChild(element)
@@ -683,7 +686,7 @@ class ShowContent extends Content {
input.setAttribute("id", "searchMapping")
input.setAttribute("placeholder", "{{.button.search}}")
input.className = "search"
input.setAttribute("onchange", 'javascript: searchInMapping()')
input.setAttribute("oninput", 'javascript: searchInMapping()')
interaction.appendChild(input)
break;
@@ -824,16 +827,227 @@ class ShowContent extends Content {
}
var SHELL_LAYOUT_READY:boolean = false
function setLayoutMenuState(open:boolean) {
if (document.body == null) {
return
}
if (open == true) {
document.body.classList.add("menu-open")
} else {
document.body.classList.remove("menu-open")
}
}
function toggleLayoutMenu() {
if (document.body == null) {
return
}
var isOpen:boolean = document.body.classList.contains("menu-open")
setLayoutMenuState(!isOpen)
}
function closeLayoutMenuIfMobile() {
if (window.innerWidth <= 900) {
setLayoutMenuState(false)
}
}
function setActiveMenu(menuID:string) {
ACTIVE_MENU_ID = menuID.toString()
var menu = document.getElementById("main-menu")
if (menu == null) {
return
}
var items = menu.getElementsByTagName("LI")
for (var i = 0; i < items.length; i++) {
items[i].classList.remove("menu-active")
}
var activeItem = document.getElementById(ACTIVE_MENU_ID)
var activeMenuKey:string = ""
if (activeItem != null) {
activeItem.classList.add("menu-active")
var menuKeyValue = activeItem.getAttribute("data-menu")
if (menuKeyValue != null) {
activeMenuKey = menuKeyValue
}
}
if (document.body != null) {
if (activeMenuKey.length > 0) {
document.body.setAttribute("data-active-menu", activeMenuKey)
} else {
document.body.removeAttribute("data-active-menu")
}
if (activeMenuKey == "log") {
document.body.classList.add("menu-log-focus")
} else {
document.body.classList.remove("menu-log-focus")
}
}
}
function renderStatusCards() {
var wrapper = document.getElementById("status-cards")
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
return
}
var info = SERVER["clientInfo"]
var errors:number = parseInt(info["errors"], 10)
var warnings:number = parseInt(info["warnings"], 10)
var cards:any[] = [
{label: "Streams", value: info["streams"], tone: "ok"},
{label: "EPG Source", value: info["epgSource"], tone: "neutral"},
{label: "XEPG Channels", value: info["xepg"], tone: "ok"},
{label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok"},
{label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok"},
{label: "DVR", value: info["DVR"], tone: "neutral"},
]
wrapper.innerHTML = ""
cards.forEach(card => {
var box = document.createElement("DIV")
box.className = "status-card status-card-" + card.tone
var label = document.createElement("P")
label.className = "status-card-label"
label.innerText = card.label
var value = document.createElement("P")
value.className = "status-card-value"
if (card.value == undefined || card.value == "") {
value.innerText = "-"
} else {
value.innerText = card.value
}
box.appendChild(label)
box.appendChild(value)
wrapper.appendChild(box)
});
}
function initShellLayout() {
if (SHELL_LAYOUT_READY == true) {
return
}
var toggle = document.getElementById("menu-toggle")
if (toggle != null) {
toggle.onclick = function() {
toggleLayoutMenu()
}
}
var overlay = document.getElementById("layout-overlay")
if (overlay != null) {
overlay.onclick = function() {
setLayoutMenuState(false)
}
}
document.addEventListener("keydown", function(event) {
if (event.key == "Escape") {
setLayoutMenuState(false)
showElement("popup", false)
return
}
if (event.key == "/") {
var target = event.target as HTMLElement
var onInput = target.tagName == "INPUT" || target.tagName == "TEXTAREA" || target.tagName == "SELECT"
if (onInput == true) {
return
}
var search = document.getElementById("searchMapping")
if (search != null) {
event.preventDefault()
(search as HTMLInputElement).focus()
}
}
});
setConnectionState("idle")
SHELL_LAYOUT_READY = true
}
function shouldPollLogs():boolean {
if (document.hidden == true) {
return false
}
if (document.getElementById("content_log") == null) {
return false
}
if (ACTIVE_MENU_ID.length < 1) {
return false
}
var activeItem = document.getElementById(ACTIVE_MENU_ID)
if (activeItem == null) {
return false
}
return activeItem.getAttribute("data-menu") == "log"
}
function PageReady() {
initShellLayout()
var server:Server = new Server("getServerConfig")
server.request(new Object())
var bootstrapAttempts:number = 0
var maxBootstrapAttempts:number = 5
var bootstrapTimer:number = window.setInterval(function() {
if (SERVER.hasOwnProperty("clientInfo") == true) {
window.clearInterval(bootstrapTimer)
return
}
if (SERVER_CONNECTION == true) {
return
}
bootstrapAttempts++
var retryServer:Server = new Server("getServerConfig")
retryServer.request(new Object())
if (bootstrapAttempts >= maxBootstrapAttempts) {
window.clearInterval(bootstrapTimer)
}
}, 3000)
window.addEventListener("resize", function(){
if (window.innerWidth > 900) {
setLayoutMenuState(false)
}
calculateWrapperHeight();
}, true);
setInterval(function(){
if (shouldPollLogs() == false) {
return
}
updateLog()
}, 10000);
@@ -841,6 +1055,35 @@ function PageReady() {
return
}
function isClientInfoHttpURL(value:string):boolean {
return /^https?:\/\//i.test(value)
}
function setClientInfoValue(key:string, value:any) {
var element = document.getElementById(key)
if (element == null) {
return
}
var textValue = ""
if (value != undefined && value != null) {
textValue = String(value)
}
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
element.innerHTML = ""
var anchor = document.createElement("A")
anchor.href = textValue
anchor.target = "_blank"
anchor.rel = "noopener noreferrer"
anchor.textContent = textValue
element.appendChild(anchor)
return
}
element.innerHTML = textValue
}
function createLayout() {
// Client Info
@@ -848,11 +1091,10 @@ function createLayout() {
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]];
}
setClientInfoValue(keys[i], obj[keys[i]])
}
renderStatusCards()
if (!document.getElementById("main-menu")) {
return
@@ -889,13 +1131,32 @@ function createLayout() {
}
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
setActiveMenu(ACTIVE_MENU_ID)
}
var content = document.getElementById("content")
var menu = document.getElementById("main-menu")
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
var firstItem = menu.getElementsByTagName("LI")[0]
if (firstItem != undefined) {
firstItem.click()
}
}
}
return
}
function openThisMenu(element) {
var id = element.id
var content:ShowContent = new ShowContent(id)
setActiveMenu(id)
content.show()
closeLayoutMenuIfMobile()
calculateWrapperHeight()
return

View File

@@ -18,6 +18,7 @@ class Server {
if (this.cmd != "updateLog") {
showElement("loading", true)
UNDO = new Object()
setConnectionState("busy")
}
switch(window.location.protocol) {
@@ -29,13 +30,67 @@ class Server {
break
}
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token")
var wsHost:string = window.location.host
if (wsHost == undefined || wsHost.length < 1) {
wsHost = window.location.hostname
}
var url = this.protocol + wsHost + "/data/" + "?Token=" + getCookie("Token")
data["cmd"] = this.cmd
var requestCmd:string = data["cmd"]
var ws = new WebSocket(url)
var isLogUpdate:boolean = data["cmd"] == "updateLog"
var responseReceived:boolean = false
var requestFinished:boolean = false
var timeoutMs:number = 12000
var requestTimeout:number
var finishRequest = function(state:string, responseSuccess:boolean = false):void {
if (requestFinished == true) {
return
}
requestFinished = true
SERVER_CONNECTION = false
window.clearTimeout(requestTimeout)
if (responseSuccess == true) {
if (state == "online") {
WS_FAILURE_COUNT = 0
}
} else {
WS_FAILURE_COUNT++
}
if (isLogUpdate == false) {
showElement("loading", false)
}
if (state != "") {
setConnectionState(state)
}
}
requestTimeout = window.setTimeout(function() {
console.log("Websocket request timed out.")
var timeoutState:string = "offline"
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
timeoutState = "idle"
}
finishRequest(timeoutState, false)
try {
ws.close()
} catch (err) {
console.log(err)
}
}, timeoutMs)
ws.onopen = function() {
WS_AVAILABLE = true
if (data["cmd"] != "updateLog") {
setConnectionState("busy")
}
console.log("REQUEST (JS):");
console.log(data)
@@ -50,9 +105,13 @@ class Server {
ws.onerror = function(e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.")
SERVER_CONNECTION = false
var errorState:string = "offline"
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
errorState = "idle"
}
finishRequest(errorState, false)
if (WS_AVAILABLE == false) {
if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
}
@@ -60,9 +119,8 @@ class Server {
ws.onmessage = function (e) {
SERVER_CONNECTION = false
showElement("loading", false)
responseReceived = true
finishRequest("online", true)
console.log("RESPONSE:");
var response = JSON.parse(e.data);
@@ -74,6 +132,7 @@ class Server {
}
if (response["status"] == false) {
setConnectionState("offline")
alert(response["err"])
@@ -136,10 +195,24 @@ class Server {
}
ws.onclose = function() {
if (responseReceived == true) {
return
}
var closeState:string = "offline"
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
closeState = "idle"
}
finishRequest(closeState, false)
}
}
}
var WS_FAILURE_COUNT:number = 0
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");

View File

@@ -75,6 +75,35 @@ class SettingsCategory {
setting.appendChild(tdRight)
break
case "plex.url":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.plexURL.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "plex.url", data)
input.setAttribute("placeholder", "{{.settings.plexURL.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "plex.token":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.plexToken.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("password", "plex.token", data)
input.setAttribute("placeholder", "{{.settings.plexToken.placeholder}}")
input.setAttribute("autocomplete", "off")
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}}" + ":"
@@ -286,10 +315,40 @@ class SettingsCategory {
setting.appendChild(tdRight)
break
// Select
case "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
case "use_plexAPI":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.usePlexAPI.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 "xepg.missing.epg.mode":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.xepgMissingEPGMode.title}}" + ":"
var tdRight = document.createElement("TD")
var text:any[] = ["{{.settings.xepgMissingEPGMode.info_strict}}", "{{.settings.xepgMissingEPGMode.info_relaxed}}"]
var values:any[] = ["strict", "relaxed"]
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 "tuner":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
var tdRight = document.createElement("TD")
var text = new Array()
@@ -454,6 +513,14 @@ class SettingsCategory {
text = "{{.settings.userAgent.description}}"
break
case "plex.url":
text = "{{.settings.plexURL.description}}"
break
case "plex.token":
text = "{{.settings.plexToken.description}}"
break
case "ffmpeg.path":
text = "{{.settings.ffmpegPath.description}}"
break
@@ -486,6 +553,10 @@ class SettingsCategory {
text = "{{.settings.api.description}}"
break
case "use_plexAPI":
text = "{{.settings.usePlexAPI.description}}"
break
case "files.update":
text = "{{.settings.filesUpdate.description}}"
break
@@ -494,12 +565,16 @@ class SettingsCategory {
text = "{{.settings.cacheImages.description}}"
break
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"
break
case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"
break
case "udpxy":
text = "{{.settings.udpxy.description}}"
case "xepg.missing.epg.mode":
text = "{{.settings.xepgMissingEPGMode.description}}"
break
case "udpxy":
text = "{{.settings.udpxy.description}}"
break
default:

View File

@@ -39,7 +39,8 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
const Name = "xTeVe"
// Version : Version, die Build Nummer wird in der main func geparst.
const Version = "2.2.0.0200"
// Can be overwritten at build time: -ldflags "-X main.Version=..."
var Version = "2.2.0.0200"
// DBVersion : Datanbank Version
const DBVersion = "2.1.0"