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 ## Downloads v2 | 64 Bit only
#### 64 Bit Intel / AMD #### 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 #### 2.1.1.0116-beta
If no user agent is specified, the default FFmpeg or VLC user agent is used. 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 module xteve
go 1.16 go 1.25
require ( require (
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.5.3
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/koron/go-ssdp v0.0.2 // indirect 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 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 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 h1:fL3wAoyT6hXHQlORyXUW4Q23kkQpJRgEAYcZB5BR71o=
github.com/koron/go-ssdp v0.0.2/go.mod h1:XoLfkAiA2KeZsYh4DbHxD7h3nR2AZNqVQOa+LJuqPYs= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-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 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 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-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-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 h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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= 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> <script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
</head> </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 class="loader"></div>
</div> </div>
<div id="header" class="imgCenter"></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> <tr>
<td class="tdKey">Version:</td> <td class="tdKey">Version:</td>
<td id="version" class="tdVal">&nbsp;</td> <td id="version" class="tdVal">&nbsp;</td>
@@ -46,13 +46,14 @@
<div id="headline"> <div id="headline">
<h1 id="head-text" class="center">Configuration</h1> <h1 id="head-text" class="center">Configuration</h1>
</div> </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"> <div id="content" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Configuration step">
</div> </div>
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
<div id="box-footer"> <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> </div>
</main>
</body> </body>
</html> </html>

View File

@@ -10,38 +10,38 @@
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script> <script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
</head> </head>
<body> <body class="auth-screen">
<div id="header" class="imgCenter"></div> <div id="header" class="imgCenter"></div>
<div id="box"> <main id="box" role="main" aria-labelledby="head-text">
<div id="headline"> <div id="headline">
<h1 id="head-text" class="center">{{.account.headline}}</h1> <h1 id="head-text" class="center">{{.account.headline}}</h1>
</div> </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"> <div id="content">
<form id="authentication" action="" method="post"> <form id="authentication" action="" method="post" aria-describedby="err" novalidate>
<h5>{{.account.username.title}}:</h5> <label for="username">{{.account.username.title}}:</label>
<input id="username" type="text" name="username" placeholder="Username" value=""> <input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
<h5>{{.account.password.title}}:</h5> <label for="password">{{.account.password.title}}:</label>
<input id="password" type="password" name="password" placeholder="Password" value=""> <input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="new-password">
<h5>{{.account.confirm.title}}:</h5> <label for="confirm">{{.account.confirm.title}}:</label>
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value=""> <input id="confirm" type="password" name="confirm" placeholder="Confirm" value="" autocomplete="new-password">
</form> </form>
</div> </div>
<div id="box-footer"> <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>
</div> </main>
</body> </body>
</html> </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> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<!---
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-->
<title>xTeVe</title> <title>xTeVe</title>
<link rel="stylesheet" href="css/screen.css" type="text/css"> <link rel="stylesheet" href="css/screen.css" type="text/css">
<link rel="stylesheet" href="css/base.css" type="text/css"> <link rel="stylesheet" href="css/base.css" type="text/css">
@@ -17,16 +15,19 @@
</head> </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 class="loader"></div>
</div> </div>
<div id="popup" class="none"> <div id="popup" class="none" role="dialog" aria-modal="true" aria-hidden="true" tabindex="-1">
<div id="popup-custom"></div> <div id="popup-custom" role="document" tabindex="-1"></div>
</div> </div>
<div id="layout-overlay" aria-hidden="true" tabindex="-1"></div>
<div id="layout"> <div id="layout">
<!-- <!--
@@ -40,55 +41,62 @@
</div> </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= "branch"></div>
<div id="logo"></div> <div id="logo"></div>
<nav id="main-menu"></nav> <nav id="main-menu" role="menubar" aria-label="Main navigation"></nav>
</div> </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> <tr>
<td class="tdKey">xTeVe:</td> <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 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 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>
<tr> <tr>
<td class="tdKey">UUID:</td> <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 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 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>
<tr> <tr>
<td class="tdKey">Available Streams:</td> <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 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 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>
<tr> <tr>
<td class="tdKey">XEPG Channels:</td> <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 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 class="tdKey">Warnings:</td>
<td id="warnings" class="tdVal">&nbsp;</td> <td id="warnings" class="tdVal" data-label="Warnings">&nbsp;</td>
</tr> </tr>
</table> </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"> <div id="allStreams">
<table id="activeStreams"></table> <table id="activeStreams"></table>
@@ -97,9 +105,10 @@
</div> </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> </div>

View File

@@ -1,8 +1,13 @@
function login() { function login() {
var err = false; var err = false;
var firstInvalid = null;
var data = new Object(); var data = new Object();
var div = document.getElementById("content"); var div = document.getElementById("content");
var form = document.getElementById("authentication"); var form = document.getElementById("authentication");
var errElement = document.getElementById("err");
if (errElement != null) {
errElement.innerHTML = "";
}
var inputs = div.getElementsByTagName("INPUT"); var inputs = div.getElementsByTagName("INPUT");
console.log(inputs); console.log(inputs);
for (var i = inputs.length - 1; i >= 0; i--) { for (var i = inputs.length - 1; i >= 0; i--) {
@@ -10,23 +15,54 @@ function login() {
var value = inputs[i].value; var value = inputs[i].value;
if (value.length == 0) { if (value.length == 0) {
inputs[i].style.borderColor = "red"; inputs[i].style.borderColor = "red";
inputs[i].setAttribute("aria-invalid", "true");
if (firstInvalid == null) {
firstInvalid = inputs[i];
}
err = true; err = true;
} }
else {
inputs[i].style.borderColor = "";
inputs[i].setAttribute("aria-invalid", "false");
}
data[key] = value; data[key] = value;
} }
if (err == true) { if (err == true) {
if (firstInvalid != null) {
firstInvalid.focus();
}
data = new Object(); data = new Object();
return; return;
} }
if (data.hasOwnProperty("confirm")) { if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) { if (data["confirm"] != data["password"]) {
alert("sdafsd");
document.getElementById('password').style.borderColor = "red"; document.getElementById('password').style.borderColor = "red";
document.getElementById('confirm').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("err").innerHTML = "{{.account.failed}}";
document.getElementById('password').focus();
return; return;
} }
} }
console.log(data); console.log(data);
form.submit(); 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 UNDO = new Object();
var SERVER_CONNECTION = false; var SERVER_CONNECTION = false;
var WS_AVAILABLE = 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(); var menuItems = new Array();
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}")); menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"));
//menuItems.push(new MainMenuItem("pmsID", "{{.mainMenu.item.pmsID}}", "number.png", "{{.mainMenu.headline.pmsID}}")) //menuItems.push(new MainMenuItem("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}}")); menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"));
// Kategorien für die Einstellungen // Kategorien für die Einstellungen
var settingsCategory = new Array(); var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api")); 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.replace.missing.images")); 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.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.backup}}", "backup.path,backup.keep"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api")); 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"; cssClass = "none";
break; 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) { function changeButtonAction(element, buttonID, attribute) {
var value = element.options[element.selectedIndex].value; var value = element.options[element.selectedIndex].value;
@@ -114,6 +184,48 @@ function getAllSelectedChannels() {
} }
return channels; 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() { function selectAllChannels() {
var bulk = false; var bulk = false;
var trs = document.getElementById("content_table").getElementsByTagName("TR"); var trs = document.getElementById("content_table").getElementsByTagName("TR");
@@ -132,6 +244,7 @@ function selectAllChannels() {
} }
} }
} }
LAST_BULK_CHECKBOX = null;
return; return;
} }
function bulkEdit() { function bulkEdit() {
@@ -150,6 +263,7 @@ function bulkEdit() {
rows[i].className = className; rows[i].className = className;
rows[i].checked = false; rows[i].checked = false;
} }
LAST_BULK_CHECKBOX = null;
return; return;
} }
function sortTable(column) { function sortTable(column) {
@@ -160,15 +274,29 @@ function sortTable(column) {
var table = document.getElementById("content_table"); var table = document.getElementById("content_table");
var tableHead = table.getElementsByTagName("TR")[0]; var tableHead = table.getElementsByTagName("TR")[0];
var tableItems = tableHead.getElementsByTagName("TD"); 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 sortObj = new Object();
var x, xValue; var x, xValue;
var tableHeader; var tableHeader;
var sortByString = false; var sortByString = false;
if (column > 0 && COLUMN_TO_SORT > 0) { if (column > 0 && COLUMN_TO_SORT > 0) {
tableItems[COLUMN_TO_SORT].className = "pointer"; tableItems[COLUMN_TO_SORT].classList.remove("sortThis");
tableItems[column].className = "sortThis"; tableItems[COLUMN_TO_SORT].classList.add("pointer");
tableItems[column].classList.remove("pointer");
tableItems[column].classList.add("sortThis");
} }
COLUMN_TO_SORT = column; 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; var rows = table.rows;
if (rows[1] != undefined) { if (rows[1] != undefined) {
tableHeader = rows[0]; tableHeader = rows[0];
@@ -228,6 +356,7 @@ function createSearchObj() {
var channels = getObjKeys(data); var channels = getObjKeys(data);
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"]; var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"];
channels.forEach(function (id) { channels.forEach(function (id) {
SEARCH_MAPPING[id] = "";
channelKeys.forEach(function (key) { channelKeys.forEach(function (key) {
if (key == "x-active") { if (key == "x-active") {
switch (data[id][key]) { switch (data[id][key]) {
@@ -260,6 +389,9 @@ function searchInMapping() {
for (var i = 1; i < trs.length; ++i) { for (var i = 1; i < trs.length; ++i) {
var id = trs[i].getAttribute("id"); var id = trs[i].getAttribute("id");
var element = SEARCH_MAPPING[id]; var element = SEARCH_MAPPING[id];
if (element == undefined) {
continue;
}
switch (element.toLowerCase().includes(searchValue.toLowerCase())) { switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
case true: case true:
document.getElementById(id).style.display = ""; document.getElementById(id).style.display = "";
@@ -269,17 +401,19 @@ function searchInMapping() {
break; break;
} }
} }
announceToScreenReader("Search updated");
return; return;
} }
function calculateWrapperHeight() { function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")) {
var elm = document.getElementById("box-wrapper"); var elm = document.getElementById("box-wrapper");
var divs = new Array("myStreamsBox", "clientInfo", "content"); var content = document.getElementById("content");
var elementsHeight = 0 - elm.offsetHeight; if (elm != null && content != null) {
for (var i = 0; i < divs.length; i++) { var contentTop = content.getBoundingClientRect().top;
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight; var freeSpace = window.innerHeight - contentTop - 26;
if (freeSpace < 180) {
freeSpace = 180;
} }
elm.style.height = window.innerHeight - elementsHeight + "px"; elm.style.height = freeSpace + "px";
} }
return; return;
} }

View File

@@ -35,7 +35,9 @@ var WizardItem = /** @class */ (function (_super) {
var key = this.key; var key = this.key;
var content = new PopupContent(); var content = new PopupContent();
var description; var description;
var wizardField = null;
var doc = document.getElementById(this.DocumentID); var doc = document.getElementById(this.DocumentID);
doc.setAttribute("aria-busy", "true");
doc.innerHTML = ""; doc.innerHTML = "";
doc.appendChild(headline); doc.appendChild(headline);
switch (key) { switch (key) {
@@ -50,6 +52,7 @@ var WizardItem = /** @class */ (function (_super) {
select.setAttribute("class", "wizard"); select.setAttribute("class", "wizard");
select.id = key; select.id = key;
doc.appendChild(select); doc.appendChild(select);
wizardField = select;
description = "{{.wizard.tuner.description}}"; description = "{{.wizard.tuner.description}}";
break; break;
case "epgSource": case "epgSource":
@@ -59,6 +62,7 @@ var WizardItem = /** @class */ (function (_super) {
select.setAttribute("class", "wizard"); select.setAttribute("class", "wizard");
select.id = key; select.id = key;
doc.appendChild(select); doc.appendChild(select);
wizardField = select;
description = "{{.wizard.epgSource.description}}"; description = "{{.wizard.epgSource.description}}";
break; break;
case "m3u": case "m3u":
@@ -67,6 +71,7 @@ var WizardItem = /** @class */ (function (_super) {
input.setAttribute("class", "wizard"); input.setAttribute("class", "wizard");
input.id = key; input.id = key;
doc.appendChild(input); doc.appendChild(input);
wizardField = input;
description = "{{.wizard.m3u.description}}"; description = "{{.wizard.m3u.description}}";
break; break;
case "xmltv": case "xmltv":
@@ -75,6 +80,7 @@ var WizardItem = /** @class */ (function (_super) {
input.setAttribute("class", "wizard"); input.setAttribute("class", "wizard");
input.id = key; input.id = key;
doc.appendChild(input); doc.appendChild(input);
wizardField = input;
description = "{{.wizard.xmltv.description}}"; description = "{{.wizard.xmltv.description}}";
break; break;
default: default:
@@ -82,8 +88,20 @@ var WizardItem = /** @class */ (function (_super) {
break; break;
} }
var pre = document.createElement("PRE"); var pre = document.createElement("PRE");
pre.id = "wizard-description-" + key;
pre.innerHTML = description; pre.innerHTML = description;
doc.appendChild(pre); 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); console.log(headline, key);
}; };
return WizardItem; 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("epgSource", "{{.wizard.epgSource.title}}"));
configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}")); configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}"));
configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.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 log = new Log();
var logs = SERVER["log"]["log"]; var logs = SERVER["log"]["log"];
var div = document.getElementById("content_log"); var div = document.getElementById("content_log");
var wrapper = document.getElementById("box-wrapper");
var shouldStickToBottom = bottom;
div.innerHTML = ""; div.innerHTML = "";
if (wrapper != null && shouldStickToBottom == false) {
var distanceToBottom = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight;
if (distanceToBottom < 80) {
shouldStickToBottom = true;
}
}
var keys = getObjKeys(logs); var keys = getObjKeys(logs);
keys.forEach(function (logID) { keys.forEach(function (logID) {
var entry = log.createLog(logs[logID]); var entry = log.createLog(logs[logID]);
div.append(entry); div.append(entry);
}); });
setTimeout(function () { setTimeout(function () {
if (bottom == true) { if (shouldStickToBottom == true && wrapper != null) {
var wrapper = document.getElementById("box-wrapper");
wrapper.scrollTop = wrapper.scrollHeight; wrapper.scrollTop = wrapper.scrollHeight;
} }
}, 10); }, 10);

View File

@@ -43,7 +43,14 @@ var MainMenuItem = /** @class */ (function (_super) {
var item = document.createElement("LI"); var item = document.createElement("LI");
item.setAttribute("onclick", "javascript: openThisMenu(this)"); item.setAttribute("onclick", "javascript: openThisMenu(this)");
item.setAttribute("id", this.id); 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); var img = this.createIMG(this.imgSrc);
img.setAttribute("alt", "");
var value = this.createValue(this.value); var value = this.createValue(this.value);
item.appendChild(img); item.appendChild(img);
item.appendChild(value); 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}}"]; this.tableHeader = ["{{.users.table.username}}", "{{.users.table.password}}", "{{.users.table.web}}", "{{.users.table.pms}}", "{{.users.table.m3u}}", "{{.users.table.xml}}", "{{.users.table.api}}"];
break; break;
case "mapping": 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; break;
} }
//console.log(this.menuKey, this.tableHeader); //console.log(this.menuKey, this.tableHeader);
@@ -356,39 +363,27 @@ var Content = /** @class */ (function () {
cell.child = true; cell.child = true;
cell.childType = "IMG"; cell.childType = "IMG";
cell.imageURL = data[key]["tvg-logo"]; cell.imageURL = data[key]["tvg-logo"];
var td = cell.createCell(); tr.appendChild(cell.createCell());
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
// Kanalname // Kanalname
var cell = new Cell(); var cell = new Cell();
cell.child = true; cell.child = true;
cell.childType = "P"; cell.childType = "P";
cell.className = data[key]["x-category"]; cell.className = data[key]["x-category"];
cell.value = data[key]["x-name"]; cell.value = data[key]["x-name"];
var td = cell.createCell(); tr.appendChild(cell.createCell());
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
// Playlist // Playlist
var cell = new Cell(); var cell = new Cell();
cell.child = true; cell.child = true;
cell.childType = "P"; cell.childType = "P";
//cell.value = data[key]["_file.m3u.name"] //cell.value = data[key]["_file.m3u.name"]
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name"); cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name");
var td = cell.createCell(); tr.appendChild(cell.createCell());
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
// Gruppe (group-title) // Gruppe (group-title)
var cell = new Cell(); var cell = new Cell();
cell.child = true; cell.child = true;
cell.childType = "P"; cell.childType = "P";
cell.value = data[key]["x-group-title"]; cell.value = data[key]["x-group-title"];
var td = cell.createCell(); tr.appendChild(cell.createCell());
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
// XMLTV Datei // XMLTV Datei
var cell = new Cell(); var cell = new Cell();
cell.child = true; cell.child = true;
@@ -399,10 +394,7 @@ var Content = /** @class */ (function () {
else { else {
cell.value = data[key]["x-xmltv-file"]; cell.value = data[key]["x-xmltv-file"];
} }
var td = cell.createCell(); tr.appendChild(cell.createCell());
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
td.id = key;
tr.appendChild(td);
// XMLTV Kanal // XMLTV Kanal
var cell = new Cell(); var cell = new Cell();
cell.child = true; cell.child = true;
@@ -413,10 +405,17 @@ var Content = /** @class */ (function () {
value = data[key]["x-mapping"].substring(0, 20) + "..."; value = data[key]["x-mapping"].substring(0, 20) + "...";
} }
cell.value = value; cell.value = value;
var td = cell.createCell(); tr.appendChild(cell.createCell());
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)'); var cell = new Cell();
td.id = key; cell.child = true;
tr.appendChild(td); 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); rows.push(tr);
}); });
break; break;
@@ -451,7 +450,7 @@ var Cell = /** @class */ (function () {
break; break;
case "INPUTCHANNEL": case "INPUTCHANNEL":
element = document.createElement("INPUT"); element = document.createElement("INPUT");
element.setAttribute("onchange", "javscript: changeChannelNumber(this)"); element.setAttribute("onchange", "javascript: changeChannelNumber(this)");
element.value = this.value; element.value = this.value;
element.type = "text"; element.type = "text";
break; break;
@@ -460,6 +459,9 @@ var Cell = /** @class */ (function () {
element.checked = this.value; element.checked = this.value;
element.type = "checkbox"; element.type = "checkbox";
element.className = "bulk hideBulk"; element.className = "bulk hideBulk";
element.addEventListener("click", function (event) {
scheduleChannelRangeSelection(element, event);
});
break; break;
case "BULK_HEAD": case "BULK_HEAD":
element = document.createElement("INPUT"); element = document.createElement("INPUT");
@@ -475,6 +477,12 @@ var Cell = /** @class */ (function () {
element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''"); element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''");
//onerror="this.onerror=null;this.src='missing.gif';" //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); td.appendChild(element);
} }
@@ -483,11 +491,19 @@ var Cell = /** @class */ (function () {
} }
if (this.onclick == true) { if (this.onclick == true) {
td.setAttribute("onclick", this.onclickFunktion); 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) { if (this.tdClassName != undefined) {
if (td.className.length > 0) {
td.className = td.className + " " + this.tdClassName;
}
else {
td.className = this.tdClassName; td.className = this.tdClassName;
} }
}
return td; return td;
}; };
return Cell; return Cell;
@@ -510,6 +526,7 @@ var ShowContent = /** @class */ (function (_super) {
COLUMN_TO_SORT = -1; COLUMN_TO_SORT = -1;
// Alten Inhalt löschen // Alten Inhalt löschen
var doc = document.getElementById(this.DocumentID); var doc = document.getElementById(this.DocumentID);
doc.setAttribute("aria-busy", "true");
doc.innerHTML = ""; doc.innerHTML = "";
showPreview(false); showPreview(false);
// Überschrift // Überschrift
@@ -556,11 +573,33 @@ var ShowContent = /** @class */ (function (_super) {
var input = this.createInput("button", menuKey, "{{.button.bulkEdit}}"); var input = this.createInput("button", menuKey, "{{.button.bulkEdit}}");
input.setAttribute("onclick", 'javascript: bulkEdit()'); input.setAttribute("onclick", 'javascript: bulkEdit()');
interaction.appendChild(input); 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", ""); var input = this.createInput("search", "search", "");
input.setAttribute("id", "searchMapping"); input.setAttribute("id", "searchMapping");
input.setAttribute("placeholder", "{{.button.search}}"); input.setAttribute("placeholder", "{{.button.search}}");
input.setAttribute("aria-label", "{{.button.search}}");
input.className = "search"; input.className = "search";
input.setAttribute("onchange", 'javascript: searchInMapping()'); input.setAttribute("oninput", 'javascript: searchInMapping()');
interaction.appendChild(input); interaction.appendChild(input);
break; break;
case "settings": case "settings":
@@ -580,6 +619,7 @@ var ShowContent = /** @class */ (function (_super) {
var settings = this.createDIV(); var settings = this.createDIV();
wrapper.appendChild(settings); wrapper.appendChild(settings);
showSettings(); showSettings();
finalizeContentAccessibility(headline);
return; return;
break; break;
case "log": case "log":
@@ -593,6 +633,7 @@ var ShowContent = /** @class */ (function (_super) {
var logs = this.createDIV(); var logs = this.createDIV();
wrapper.appendChild(logs); wrapper.appendChild(logs);
showLogs(true); showLogs(true);
finalizeContentAccessibility(headline);
return; return;
break; break;
case "logout": case "logout":
@@ -665,30 +706,392 @@ var ShowContent = /** @class */ (function (_super) {
break; break;
} }
showElement("loading", false); showElement("loading", false);
finalizeContentAccessibility(headline);
}; };
return ShowContent; return ShowContent;
}(Content)); }(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() { function PageReady() {
initShellLayout();
var server = new Server("getServerConfig"); var server = new Server("getServerConfig");
server.request(new Object()); 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 () { window.addEventListener("resize", function () {
if (window.innerWidth > 900) {
setLayoutMenuState(false);
}
calculateWrapperHeight(); calculateWrapperHeight();
}, true); }, true);
setInterval(function () { setInterval(function () {
if (shouldPollLogs() == false) {
return;
}
updateLog(); updateLog();
}, 10000); }, 10000);
return; 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() { function createLayout() {
var contentRegion = document.getElementById("content");
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "true");
}
// Client Info // Client Info
var obj = SERVER["clientInfo"]; var obj = SERVER["clientInfo"];
var keys = getObjKeys(obj); var keys = getObjKeys(obj);
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (document.getElementById(keys[i])) { setClientInfoValue(keys[i], obj[keys[i]]);
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
}
} }
renderStatusCards();
if (!document.getElementById("main-menu")) { if (!document.getElementById("main-menu")) {
if (contentRegion != null) {
contentRegion.setAttribute("aria-busy", "false");
}
return; return;
} }
// Menü erstellen // Menü erstellen
@@ -713,12 +1116,35 @@ function createLayout() {
break; 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; return;
} }
function openThisMenu(element) { function openThisMenu(element) {
var id = element.id; var id = element.id;
var content = new ShowContent(id); var content = new ShowContent(id);
setActiveMenu(id);
content.show(); content.show();
var contentArea = document.getElementById("content");
if (contentArea != null) {
contentArea.scrollTop = 0;
}
closeLayoutMenuIfMobile();
calculateWrapperHeight(); calculateWrapperHeight();
return; return;
} }
@@ -756,9 +1182,26 @@ var PopupContent = /** @class */ (function (_super) {
} }
PopupContent.prototype.createHeadline = function (headline) { PopupContent.prototype.createHeadline = function (headline) {
this.doc.innerHTML = ""; this.doc.innerHTML = "";
var titleBar = document.createElement("DIV");
titleBar.className = "popup-title";
var element = document.createElement("H3"); var element = document.createElement("H3");
element.id = "popup-title-text";
element.innerHTML = headline.toUpperCase(); 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 // Tabelle erstellen
this.table = document.createElement("TABLE"); this.table = document.createElement("TABLE");
this.doc.appendChild(this.table); this.doc.appendChild(this.table);

View File

@@ -11,6 +11,7 @@ var Server = /** @class */ (function () {
if (this.cmd != "updateLog") { if (this.cmd != "updateLog") {
showElement("loading", true); showElement("loading", true);
UNDO = new Object(); UNDO = new Object();
setConnectionState("busy");
} }
switch (window.location.protocol) { switch (window.location.protocol) {
case "http:": case "http:":
@@ -20,11 +21,61 @@ var Server = /** @class */ (function () {
this.protocol = "wss://"; this.protocol = "wss://";
break; 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; data["cmd"] = this.cmd;
var requestCmd = data["cmd"];
var ws = new WebSocket(url); 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.onopen = function () {
WS_AVAILABLE = true; WS_AVAILABLE = true;
if (data["cmd"] != "updateLog") {
setConnectionState("busy");
}
console.log("REQUEST (JS):"); console.log("REQUEST (JS):");
console.log(data); console.log(data);
console.log("REQUEST: (JSON)"); console.log("REQUEST: (JSON)");
@@ -33,14 +84,18 @@ var Server = /** @class */ (function () {
}; };
ws.onerror = function (e) { ws.onerror = function (e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration."); console.log("No websocket connection to xTeVe could be established. Check your network configuration.");
SERVER_CONNECTION = false; var errorState = "offline";
if (WS_AVAILABLE == false) { 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."); alert("No websocket connection to xTeVe could be established. Check your network configuration.");
} }
}; };
ws.onmessage = function (e) { ws.onmessage = function (e) {
SERVER_CONNECTION = false; responseReceived = true;
showElement("loading", false); finishRequest("online", true);
console.log("RESPONSE:"); console.log("RESPONSE:");
var response = JSON.parse(e.data); var response = JSON.parse(e.data);
console.log(response); console.log(response);
@@ -48,6 +103,7 @@ var Server = /** @class */ (function () {
document.cookie = "Token=" + response["token"]; document.cookie = "Token=" + response["token"];
} }
if (response["status"] == false) { if (response["status"] == false) {
setConnectionState("offline");
alert(response["err"]); alert(response["err"]);
if (response.hasOwnProperty("reload")) { if (response.hasOwnProperty("reload")) {
location.reload(); location.reload();
@@ -94,9 +150,20 @@ var Server = /** @class */ (function () {
} }
createLayout(); 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; return Server;
}()); }());
var WS_FAILURE_COUNT = 0;
function getCookie(name) { function getCookie(name) {
var value = "; " + document.cookie; var value = "; " + document.cookie;
var parts = value.split("; " + name + "="); var parts = value.split("; " + name + "=");

View File

@@ -74,6 +74,29 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft); setting.appendChild(tdLeft);
setting.appendChild(tdRight); setting.appendChild(tdRight);
break; 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": case "buffer.timeout":
var tdLeft = document.createElement("TD"); var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"; tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":";
@@ -240,7 +263,30 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft); setting.appendChild(tdLeft);
setting.appendChild(tdRight); setting.appendChild(tdRight);
break; 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 // 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": case "tuner":
var tdLeft = document.createElement("TD"); var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"; tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":";
@@ -364,6 +410,12 @@ var SettingsCategory = /** @class */ (function () {
case "user.agent": case "user.agent":
text = "{{.settings.userAgent.description}}"; text = "{{.settings.userAgent.description}}";
break; break;
case "plex.url":
text = "{{.settings.plexURL.description}}";
break;
case "plex.token":
text = "{{.settings.plexToken.description}}";
break;
case "ffmpeg.path": case "ffmpeg.path":
text = "{{.settings.ffmpegPath.description}}"; text = "{{.settings.ffmpegPath.description}}";
break; break;
@@ -388,6 +440,9 @@ var SettingsCategory = /** @class */ (function () {
case "api": case "api":
text = "{{.settings.api.description}}"; text = "{{.settings.api.description}}";
break; break;
case "use_plexAPI":
text = "{{.settings.usePlexAPI.description}}";
break;
case "files.update": case "files.update":
text = "{{.settings.filesUpdate.description}}"; text = "{{.settings.filesUpdate.description}}";
break; break;
@@ -397,6 +452,9 @@ var SettingsCategory = /** @class */ (function () {
case "xepg.replace.missing.images": case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"; text = "{{.settings.replaceEmptyImages.description}}";
break; break;
case "xepg.missing.epg.mode":
text = "{{.settings.xepgMissingEPGMode.description}}";
break;
case "udpxy": case "udpxy":
text = "{{.settings.udpxy.description}}"; text = "{{.settings.udpxy.description}}";
break; break;

View File

@@ -41,6 +41,7 @@
"backup": "Backup", "backup": "Backup",
"bulkEdit": "Bulk Edit", "bulkEdit": "Bulk Edit",
"cancel": "Cancel", "cancel": "Cancel",
"edit": "Edit",
"delete": "Delete", "delete": "Delete",
"done": "Done", "done": "Done",
"login": "Login", "login": "Login",
@@ -210,7 +211,8 @@
"playlist": "Playlist", "playlist": "Playlist",
"groupTitle": "Group Title", "groupTitle": "Group Title",
"xmltvFile": "XMLTV File", "xmltvFile": "XMLTV File",
"xmltvID": "XMLTV ID" "xmltvID": "XMLTV ID",
"edit": "Edit"
}, },
"active": "active":
{ {
@@ -355,6 +357,23 @@
"title": "API Interface", "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>" "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": "epgSource":
{ {
"title": "EPG Source", "title": "EPG Source",
@@ -380,6 +399,13 @@
"title": "Replace missing program images", "title": "Replace missing program images",
"description": "If the poster in the XMLTV program is missing, the channel logo will be used." "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": "xteveAutoUpdate":
{ {
"title": "Automatic update of xTeVe", "title": "Automatic update of xTeVe",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -80,6 +80,7 @@ func ShowSystemInfo() {
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate)) fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath)) fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages)) 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)) fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
println("---") println("---")

View File

@@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"crypto/hmac" "crypto/hmac"
"crypto/rand" "crypto/rand"
@@ -27,8 +28,9 @@ var database string
var databaseFile = "authentication.json" var databaseFile = "authentication.json"
var data = make(map[string]interface{}) var data = make(map[string]any)
var tokens = make(map[string]interface{}) var tokens = make(map[string]any)
var tokensMu sync.RWMutex
var initAuthentication = false var initAuthentication = false
@@ -133,10 +135,10 @@ func Init(databasePath string, validity int) (err error) {
// Check if the database already exists // Check if the database already exists
if _, err = os.Stat(database); os.IsNotExist(err) { if _, err = os.Stat(database); os.IsNotExist(err) {
// Create an empty database // Create an empty database
var defaults = make(map[string]interface{}) var defaults = make(map[string]any)
defaults["dbVersion"] = "1.0" defaults["dbVersion"] = "1.0"
defaults["hash"] = "sha256" defaults["hash"] = "sha256"
defaults["users"] = make(map[string]interface{}) defaults["users"] = make(map[string]any)
if saveDatabase(defaults) != nil { if saveDatabase(defaults) != nil {
return return
@@ -160,7 +162,7 @@ func CreateDefaultUser(username, password string) (err error) {
return return
} }
var users = data["users"].(map[string]interface{}) var users = data["users"].(map[string]any)
// Check if the default user exists // Check if the default user exists
if len(users) > 0 { if len(users) > 0 {
err = createError(001) err = createError(001)
@@ -182,7 +184,7 @@ func CreateNewUser(username, password string) (userID string, err error) {
return return
} }
var checkIfTheUserAlreadyExists = func(username string, userData map[string]interface{}) (err error) { var checkIfTheUserAlreadyExists = func(username string, userData map[string]any) (err error) {
var salt = userData["_salt"].(string) var salt = userData["_salt"].(string)
var loginUsername = userData["_username"].(string) var loginUsername = userData["_username"].(string)
@@ -193,9 +195,9 @@ func CreateNewUser(username, password string) (userID string, err error) {
return return
} }
var users = data["users"].(map[string]interface{}) var users = data["users"].(map[string]any)
for _, userData := range users { for _, userData := range users {
err = checkIfTheUserAlreadyExists(username, userData.(map[string]interface{})) err = checkIfTheUserAlreadyExists(username, userData.(map[string]any))
if err != nil { if err != nil {
return return
} }
@@ -218,7 +220,7 @@ func UserAuthentication(username, password string) (token string, err error) {
return return
} }
var login = func(username, password string, loginData map[string]interface{}) (err error) { var login = func(username, password string, loginData map[string]any) (err error) {
err = createError(010) err = createError(010)
var salt = loginData["_salt"].(string) var salt = loginData["_salt"].(string)
@@ -234,9 +236,9 @@ func UserAuthentication(username, password string) (token string, err error) {
return return
} }
var users = data["users"].(map[string]interface{}) var users = data["users"].(map[string]any)
for id, loginData := range users { for id, loginData := range users {
err = login(username, password, loginData.(map[string]interface{})) err = login(username, password, loginData.(map[string]any))
if err == nil { if err == nil {
token = setToken(id, "-") token = setToken(id, "-")
return return
@@ -256,20 +258,21 @@ func CheckTheValidityOfTheToken(token string) (newToken string, err error) {
err = createError(011) err = createError(011)
tokensMu.Lock()
defer tokensMu.Unlock()
if v, ok := tokens[token]; ok { if v, ok := tokens[token]; ok {
var expires = v.(map[string]interface{})["expires"].(time.Time) expires := v.(map[string]any)["expires"].(time.Time)
var userID = v.(map[string]interface{})["id"].(string)
if expires.Sub(time.Now().Local()) < 0 { if expires.Sub(time.Now().Local()) < 0 {
delete(tokens, token)
return return
} }
newToken = setToken(userID, token) // 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 err = nil
} else {
return
} }
return return
@@ -285,11 +288,15 @@ func GetUserID(token string) (userID string, err error) {
err = createError(002) err = createError(002)
tokensMu.Lock()
defer tokensMu.Unlock()
if v, ok := tokens[token]; ok { if v, ok := tokens[token]; ok {
var expires = v.(map[string]interface{})["expires"].(time.Time) expires := v.(map[string]any)["expires"].(time.Time)
userID = v.(map[string]interface{})["id"].(string) userID = v.(map[string]any)["id"].(string)
if expires.Sub(time.Now().Local()) < 0 { if expires.Sub(time.Now().Local()) < 0 {
delete(tokens, token)
return return
} }
@@ -300,7 +307,7 @@ func GetUserID(token string) (userID string, err error) {
} }
// WriteUserData : save user date // 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() err = checkInit()
if err != nil { if err != nil {
@@ -309,7 +316,7 @@ func WriteUserData(userID string, userData map[string]interface{}) (err error) {
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 v["data"] = userData
err = saveDatabase(data) err = saveDatabase(data)
@@ -322,7 +329,7 @@ func WriteUserData(userID string, userData map[string]interface{}) (err error) {
} }
// ReadUserData : load user date // 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() err = checkInit()
if err != nil { if err != nil {
@@ -331,8 +338,8 @@ func ReadUserData(userID string) (userData map[string]interface{}, err error) {
err = createError(031) err = createError(031)
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 {
userData = v["data"].(map[string]interface{}) userData = v["data"].(map[string]any)
err = nil err = nil
return return
@@ -351,9 +358,9 @@ func RemoveUser(userID string) (err error) {
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) delete(data["users"].(map[string]any), userID)
err = saveDatabase(data) err = saveDatabase(data)
return return
@@ -363,13 +370,13 @@ func RemoveUser(userID string) (err error) {
} }
// SetDefaultUserData : set default user data // 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 { for _, d := range allUserData {
var data = d.(map[string]interface{})["data"].(map[string]interface{}) var data = d.(map[string]any)["data"].(map[string]any)
var userID = d.(map[string]interface{})["_id"].(string) var userID = d.(map[string]any)["_id"].(string)
for k, v := range defaults { for k, v := range defaults {
if _, ok := data[k]; ok { if _, ok := data[k]; ok {
@@ -392,16 +399,16 @@ func ChangeCredentials(userID, username, password string) (err error) {
err = createError(032) err = createError(032)
if userData, ok := data["users"].(map[string]interface{})[userID]; ok { if userData, ok := data["users"].(map[string]any)[userID]; ok {
//var userData = tmp.(map[string]interface{}) //var userData = tmp.(map[string]interface{})
var salt = userData.(map[string]interface{})["_salt"].(string) var salt = userData.(map[string]any)["_salt"].(string)
if len(username) > 0 { if len(username) > 0 {
userData.(map[string]interface{})["_username"] = SHA256(username, salt) userData.(map[string]any)["_username"] = SHA256(username, salt)
} }
if len(password) > 0 { if len(password) > 0 {
userData.(map[string]interface{})["_password"] = SHA256(password, salt) userData.(map[string]any)["_password"] = SHA256(password, salt)
} }
err = saveDatabase(data) err = saveDatabase(data)
@@ -411,7 +418,7 @@ func ChangeCredentials(userID, username, password string) (err error) {
} }
// GetAllUserData : get all user data // GetAllUserData : get all user data
func GetAllUserData() (allUserData map[string]interface{}, err error) { func GetAllUserData() (allUserData map[string]any, err error) {
err = checkInit() err = checkInit()
if err != nil { if err != nil {
@@ -419,15 +426,15 @@ func GetAllUserData() (allUserData map[string]interface{}, err error) {
} }
if len(data) == 0 { if len(data) == 0 {
var defaults = make(map[string]interface{}) var defaults = make(map[string]any)
defaults["dbVersion"] = "1.0" defaults["dbVersion"] = "1.0"
defaults["hash"] = "sha256" defaults["hash"] = "sha256"
defaults["users"] = make(map[string]interface{}) defaults["users"] = make(map[string]any)
saveDatabase(defaults) saveDatabase(defaults)
data = defaults data = defaults
} }
allUserData = data["users"].(map[string]interface{}) allUserData = data["users"].(map[string]any)
return return
} }
@@ -457,7 +464,7 @@ func checkInit() (err error) {
return return
} }
func saveDatabase(tmpMap interface{}) (err error) { func saveDatabase(tmpMap any) (err error) {
jsonString, err := json.MarshalIndent(tmpMap, "", " ") jsonString, err := json.MarshalIndent(tmpMap, "", " ")
@@ -544,21 +551,26 @@ func createError(errCode int) (err error) {
return return
} }
func defaultsForNewUser(username, password string) map[string]interface{} { func defaultsForNewUser(username, password string) map[string]any {
var defaults = make(map[string]interface{}) var defaults = make(map[string]any)
var salt = randomString(saltLength) var salt = randomString(saltLength)
defaults["_username"] = SHA256(username, salt) defaults["_username"] = SHA256(username, salt)
defaults["_password"] = SHA256(password, salt) defaults["_password"] = SHA256(password, salt)
defaults["_salt"] = salt defaults["_salt"] = salt
defaults["_id"] = "id-" + randomID(idLength) defaults["_id"] = "id-" + randomID(idLength)
//defaults["_one.time.token"] = randomString(tokenLength) //defaults["_one.time.token"] = randomString(tokenLength)
defaults["data"] = make(map[string]interface{}) defaults["data"] = make(map[string]any)
return defaults return defaults
} }
func setToken(id, oldToken string) (newToken string) { func setToken(id, oldToken string) (newToken string) {
tokensMu.Lock()
defer tokensMu.Unlock()
if oldToken != "-" {
delete(tokens, oldToken) delete(tokens, oldToken)
}
loopToken: loopToken:
newToken = randomString(tokenLength) newToken = randomString(tokenLength)
@@ -566,7 +578,7 @@ loopToken:
goto loopToken goto loopToken
} }
var tmp = make(map[string]interface{}) var tmp = make(map[string]any)
tmp["id"] = id tmp["id"] = id
tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity)) tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
@@ -575,7 +587,7 @@ loopToken:
return return
} }
func mapToJSON(tmpMap interface{}) string { func mapToJSON(tmpMap any) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ") jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil { if err != nil {
return "{}" return "{}"

View File

@@ -44,7 +44,7 @@ func TestStream1(t *testing.T) {
} }
func checkStream(streamInterface []interface{}) (err error) { func checkStream(streamInterface []any) (err error) {
for i, s := range streamInterface { for i, s := range streamInterface {

View File

@@ -10,7 +10,7 @@ import (
) )
// MakeInterfaceFromM3U : // MakeInterfaceFromM3U :
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []interface{}, err error) { func MakeInterfaceFromM3U(byteStream []byte) (allChannels []any, err error) {
var content = string(byteStream) var content = string(byteStream)
var channelName string var channelName string

View File

@@ -22,10 +22,10 @@ type ClientInfo struct {
OS string `json:"os,required"` OS string `json:"os,required"`
URL string `json:"url,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 { type ServerResponse struct {
Status bool `json:"status,omitempty"` Status bool `json:"status,omitempty"`
Reason string `json:"reason,omitempty"` Reason string `json:"reason,omitempty"`
@@ -106,7 +106,7 @@ func serverRequest() (err error) {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
//fmt.Println(resp.StatusCode, Updater.URL, Updater.CMD) //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 return err
} }

View File

@@ -13,7 +13,7 @@ import (
) )
// Playlisten parsen // Playlisten parsen
func parsePlaylist(filename, fileType string) (channels []interface{}, err error) { func parsePlaylist(filename, fileType string) (channels []any, err error) {
content, err := readByteFromFile(filename) content, err := readByteFromFile(filename)
var id = strings.TrimSuffix(getFilenameFromPath(filename), path.Ext(getFilenameFromPath(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 // Streams filtern
func filterThisStream(s interface{}) (status bool) { func filterThisStream(s any) (status bool) {
status = false status = false
var stream = s.(map[string]string) 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 fileExtension, serverFileName string
var body = make([]byte, 0) var body = make([]byte, 0)
var newProvider = false 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 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 data = value
} else { } else {
data["id.provider"] = id data["id.provider"] = id
@@ -65,7 +65,7 @@ func getProviderData(fileType, fileID string) (err error) {
} }
case "compatibility": case "compatibility":
data[key] = make(map[string]interface{}) data[key] = make(map[string]any)
case "counter.download": case "counter.download":
data[key] = 0.0 data[key] = 0.0
@@ -142,7 +142,7 @@ func getProviderData(fileType, fileID string) (err error) {
for dataID, d := range dataMap { for dataID, d := range dataMap {
var data = d.(map[string]interface{}) var data = d.(map[string]any)
var fileSource = data["file.source"].(string) var fileSource = data["file.source"].(string)
newProvider = false newProvider = false
@@ -220,8 +220,8 @@ func getProviderData(fileType, fileID string) (err error) {
} }
// Fehler Counter um 1 erhöhen // Fehler Counter um 1 erhöhen
var data = make(map[string]interface{}) var data = make(map[string]any)
if value, ok := dataMap[dataID].(map[string]interface{}); ok { if value, ok := dataMap[dataID].(map[string]any); ok {
data = value data = value
data["counter.error"] = data["counter.error"].(float64) + 1 data["counter.error"] = data["counter.error"].(float64) + 1
@@ -238,9 +238,9 @@ func getProviderData(fileType, fileID string) (err error) {
// Berechnen der Fehlerquote // Berechnen der Fehlerquote
if newProvider == false { 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 data = value
if data["counter.error"].(float64) == 0 { if data["counter.error"].(float64) == 0 {
@@ -282,15 +282,23 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e
return return
} }
resp, err := http.Get(providerURL) req, err := http.NewRequest(http.MethodGet, providerURL, nil)
if err != nil { if err != nil {
return 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 { 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 return
} }

View File

@@ -323,7 +323,7 @@ func getErrMsg(errCode int) (errMsg string) {
// Tuner // Tuner
case 2105: 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: case 2106:
errMsg = fmt.Sprintf("This function is only available with XEPG as EPG source") 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 // Lineup : HDHR Lineup /lineup.json
type Lineup []interface { type Lineup []any
//GuideName string `json:"GuideName"`
//GuideNumber string `json:"GuideNumber"`
//URL string `json:"URL"`
}
// LineupStream : HDHR einzelner Stream im Lineup // LineupStream : HDHR einzelner Stream im Lineup
type LineupStream struct { type LineupStream struct {

View File

@@ -149,18 +149,18 @@ type DataStruct struct {
} }
Streams struct { Streams struct {
Active []interface{} Active []any
All []interface{} All []any
Inactive []interface{} Inactive []any
} }
XMLTV struct { XMLTV struct {
Files []string Files []string
Mapping map[string]interface{} Mapping map[string]any
} }
XEPG struct { XEPG struct {
Channels map[string]interface{} Channels map[string]any
XEPGCount int64 XEPGCount int64
} }
} }
@@ -254,6 +254,9 @@ type Notification struct {
// SettingsStruct : Inhalt der settings.json // SettingsStruct : Inhalt der settings.json
type SettingsStruct struct { type SettingsStruct struct {
API bool `json:"api"` API bool `json:"api"`
UsePlexAPI bool `json:"use_plexAPI"`
PlexURL string `json:"plex.url"`
PlexToken string `json:"plex.token"`
AuthenticationAPI bool `json:"authentication.api"` AuthenticationAPI bool `json:"authentication.api"`
AuthenticationM3U bool `json:"authentication.m3u"` AuthenticationM3U bool `json:"authentication.m3u"`
AuthenticationPMS bool `json:"authentication.pms"` AuthenticationPMS bool `json:"authentication.pms"`
@@ -275,13 +278,13 @@ 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) FileXMLTV []string `json:"xmltv,omitempty"` // Altes Speichersystem der Provider XML Datei Slice (Wird für die Umwandlung auf das neue benötigt)
Files struct { Files struct {
HDHR map[string]interface{} `json:"hdhr"` HDHR map[string]any `json:"hdhr"`
M3U map[string]interface{} `json:"m3u"` M3U map[string]any `json:"m3u"`
XMLTV map[string]interface{} `json:"xmltv"` XMLTV map[string]any `json:"xmltv"`
} `json:"files"` } `json:"files"`
FilesUpdate bool `json:"files.update"` FilesUpdate bool `json:"files.update"`
Filter map[int64]interface{} `json:"filter"` Filter map[int64]any `json:"filter"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty"`
Language string `json:"language"` Language string `json:"language"`
LogEntriesRAM int `json:"log.entries.ram"` LogEntriesRAM int `json:"log.entries.ram"`
@@ -297,8 +300,10 @@ type SettingsStruct struct {
UUID string `json:"uuid"` UUID string `json:"uuid"`
UDPxy string `json:"udpxy"` UDPxy string `json:"udpxy"`
Version string `json:"version"` Version string `json:"version"`
XepgMissingEPGMode string `json:"xepg.missing.epg.mode"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"` XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"` XteveAutoUpdate bool `json:"xteveAutoUpdate"`
WizardCompleted bool `json:"wizard.completed"`
} }
// LanguageUI : Sprache für das WebUI // LanguageUI : Sprache für das WebUI

View File

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

View File

@@ -59,7 +59,7 @@ func createSystemFiles() (err error) {
err = checkFile(filename) err = checkFile(filename)
if err != nil { if err != nil {
// Datei existiert nicht, wird jetzt erstellt // Datei existiert nicht, wird jetzt erstellt
err = saveMapToJSONFile(filename, make(map[string]interface{})) err = saveMapToJSONFile(filename, make(map[string]any))
if err != nil { if err != nil {
return return
} }
@@ -97,15 +97,28 @@ func loadSettings() (settings SettingsStruct, err error) {
return return
} }
// Deafult Werte setzten var freshInstall = len(settingsMap) == 0
var defaults = make(map[string]interface{})
var dataMap = make(map[string]interface{})
dataMap["xmltv"] = make(map[string]interface{}) // Deafult Werte setzten
dataMap["m3u"] = make(map[string]interface{}) var defaults = make(map[string]any)
dataMap["hdhr"] = make(map[string]interface{}) 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["api"] = false
defaults["use_plexAPI"] = false
defaults["plex.url"] = ""
defaults["plex.token"] = ""
defaults["authentication.api"] = false defaults["authentication.api"] = false
defaults["authentication.m3u"] = false defaults["authentication.m3u"] = false
defaults["authentication.pms"] = false defaults["authentication.pms"] = false
@@ -118,27 +131,34 @@ func loadSettings() (settings SettingsStruct, err error) {
defaults["buffer.timeout"] = 500 defaults["buffer.timeout"] = 500
defaults["cache.images"] = false defaults["cache.images"] = false
defaults["epgSource"] = "PMS" defaults["epgSource"] = "PMS"
defaults["ffmpeg.path"] = defaultFFmpegPath
defaults["ffmpeg.options"] = System.FFmpeg.DefaultOptions defaults["ffmpeg.options"] = System.FFmpeg.DefaultOptions
defaults["vlc.options"] = System.VLC.DefaultOptions defaults["vlc.options"] = System.VLC.DefaultOptions
defaults["files"] = dataMap defaults["files"] = dataMap
defaults["files.update"] = true defaults["files.update"] = true
defaults["filter"] = make(map[string]interface{}) defaults["filter"] = make(map[string]any)
defaults["git.branch"] = System.Branch defaults["git.branch"] = System.Branch
defaults["language"] = "en" defaults["language"] = "en"
defaults["log.entries.ram"] = 500 defaults["log.entries.ram"] = 500
defaults["mapping.first.channel"] = 1000 defaults["mapping.first.channel"] = 1000
defaults["xepg.missing.epg.mode"] = "strict"
defaults["xepg.replace.missing.images"] = true defaults["xepg.replace.missing.images"] = true
defaults["m3u8.adaptive.bandwidth.mbps"] = 10 defaults["m3u8.adaptive.bandwidth.mbps"] = 10
defaults["port"] = "34400" defaults["port"] = "34400"
defaults["ssdp"] = true defaults["ssdp"] = true
defaults["tuner"] = 1 defaults["tuner"] = 1
defaults["update"] = []string{"0000"} defaults["update"] = []string{"0000"}
defaults["user.agent"] = System.Name defaults["user.agent"] = defaultUserAgent
defaults["uuid"] = createUUID() defaults["uuid"] = createUUID()
defaults["udpxy"] = "" defaults["udpxy"] = ""
defaults["version"] = System.DBVersion defaults["version"] = System.DBVersion
defaults["xteveAutoUpdate"] = true 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 // Default Werte setzen
for key, value := range defaults { for key, value := range defaults {
@@ -163,8 +183,13 @@ func loadSettings() (settings SettingsStruct, err error) {
} }
if len(settings.FFmpegPath) == 0 { if len(settings.FFmpegPath) == 0 {
containerFFmpegPath := "/usr/local/bin/ffmpeg"
if len(os.Getenv("XTEVE_CONFIG")) > 0 && checkFile(containerFFmpegPath) == nil {
settings.FFmpegPath = containerFFmpegPath
} else {
settings.FFmpegPath = searchFileInOS("ffmpeg") settings.FFmpegPath = searchFileInOS("ffmpeg")
} }
}
if len(settings.VLCPath) == 0 { if len(settings.VLCPath) == 0 {
settings.VLCPath = searchFileInOS("cvlc") settings.VLCPath = searchFileInOS("cvlc")
@@ -201,8 +226,36 @@ func saveSettings(settings SettingsStruct) (err error) {
settings.BufferTimeout = 0 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) 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))) err = writeByteToFile(System.File.Settings, []byte(mapToJSON(settings)))
if err != nil { if err != nil {
return return

View File

@@ -159,7 +159,6 @@ func searchFileInOS(file string) (path string) {
return return
} }
//
func removeChildItems(dir string) error { func removeChildItems(dir string) error {
files, err := filepath.Glob(filepath.Join(dir, "*")) files, err := filepath.Glob(filepath.Join(dir, "*"))
@@ -180,7 +179,7 @@ func removeChildItems(dir string) error {
} }
// JSON // JSON
func mapToJSON(tmpMap interface{}) string { func mapToJSON(tmpMap any) string {
jsonString, err := json.MarshalIndent(tmpMap, "", " ") jsonString, err := json.MarshalIndent(tmpMap, "", " ")
if err != nil { if err != nil {
@@ -190,30 +189,30 @@ func mapToJSON(tmpMap interface{}) string {
return string(jsonString) 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) json.Unmarshal([]byte(content), &tmpMap)
return (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) json.Unmarshal([]byte(content), &tmpMap)
return (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) err = json.Unmarshal([]byte(content), &tmpMap)
return return
} }
func saveMapToJSONFile(file string, tmpMap interface{}) error { func saveMapToJSONFile(file string, tmpMap any) error {
var filename = getPlatformFile(file) var filename = getPlatformFile(file)
jsonString, err := json.MarshalIndent(tmpMap, "", " ") jsonString, err := json.MarshalIndent(tmpMap, "", " ")
@@ -230,7 +229,7 @@ func saveMapToJSONFile(file string, tmpMap interface{}) error {
return nil 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)) f, err := os.Open(getPlatformFile(file))
defer f.Close() defer f.Close()
@@ -360,7 +359,7 @@ func randomString(n int) string {
return string(bytes) 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)) 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 != http.StatusOK {
if resp.StatusCode == 404 { 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) ShowError(err, 6003)
return nil 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 return err
} }
@@ -87,7 +87,7 @@ func BinaryUpdate() (err error) {
err = up2date.GetVersion() err = up2date.GetVersion()
if err != nil { if err != nil {
debug = fmt.Sprintf(err.Error()) debug = err.Error()
showDebug(debug, 1) showDebug(debug, 1)
return nil return nil
@@ -95,7 +95,7 @@ func BinaryUpdate() (err error) {
if len(updater.Response.Reason) > 0 { 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) ShowError(err, 6002)
return nil return nil
@@ -192,7 +192,7 @@ checkVersion:
} }
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert // 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) var newFilterMap = convertToNewFilter(oldFilter)
settingsMap["filter"] = newFilterMap settingsMap["filter"] = newFilterMap
@@ -252,11 +252,11 @@ checkVersion:
return 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: case reflect.Slice:
s := reflect.ValueOf(oldFilter) s := reflect.ValueOf(oldFilter)
@@ -285,7 +285,7 @@ func setValueForUUID() (err error) {
for _, c := range xepg { for _, c := range xepg {
var xepgChannel = c.(map[string]interface{}) var xepgChannel = c.(map[string]any)
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok { 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" "github.com/gorilla/websocket"
) )
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// StartWebserver : Startet den Webserver // StartWebserver : Startet den Webserver
func StartWebserver() (err error) { 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 { if err != nil {
ShowError(err, 0) ShowError(err, 0)
http.Error(w, "Could not open websocket connection", http.StatusBadRequest) http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
@@ -558,7 +566,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
default: default:
fmt.Println("+ + + + + + + + + + +", request.Cmd) fmt.Println("+ + + + + + + + + + +", request.Cmd)
var requestMap = make(map[string]interface{}) // Debug var requestMap = make(map[string]any) // Debug
_ = requestMap _ = requestMap
if System.Dev == true { if System.Dev == true {
fmt.Println(mapToJSON(requestMap)) fmt.Println(mapToJSON(requestMap))
@@ -591,7 +599,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
// Web : Web Server /web/ // Web : Web Server /web/
func Web(w http.ResponseWriter, r *http.Request) { func Web(w http.ResponseWriter, r *http.Request) {
var lang = make(map[string]interface{}) var lang = make(map[string]any)
var err error var err error
var requestFile = strings.Replace(r.URL.Path, "/web", "html", -1) 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 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 System.ConfigurationWizard = true
} }
@@ -1028,7 +1036,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
defaults.ClientInfo.XEPGCount = Data.XEPG.XEPGCount defaults.ClientInfo.XEPGCount = Data.XEPG.XEPGCount
var XEPG = make(map[string]interface{}) var XEPG = make(map[string]any)
if len(Data.Streams.Active) > 0 { if len(Data.Streams.Active) > 0 {
@@ -1037,8 +1045,8 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
} else { } else {
XEPG["epgMapping"] = make(map[string]interface{}) XEPG["epgMapping"] = make(map[string]any)
XEPG["xmltvMap"] = make(map[string]interface{}) XEPG["xmltvMap"] = make(map[string]any)
} }

View File

@@ -9,6 +9,7 @@ import (
"path" "path"
"runtime" "runtime"
"sort" "sort"
"unicode"
"crypto/md5" "crypto/md5"
"encoding/hex" "encoding/hex"
@@ -68,6 +69,7 @@ func buildXEPG(background bool) {
cleanupXEPG() cleanupXEPG()
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg rebuild")
showInfo("XEPG:" + fmt.Sprintf("Ready to use")) showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
@@ -84,6 +86,7 @@ func buildXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg image cache refresh")
System.ImageCachingInProgress = 0 System.ImageCachingInProgress = 0
@@ -113,6 +116,7 @@ func buildXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg rebuild")
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 { if Settings.CacheImages == true && System.ImageCachingInProgress == 0 {
@@ -127,6 +131,7 @@ func buildXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg image cache refresh")
System.ImageCachingInProgress = 0 System.ImageCachingInProgress = 0
@@ -179,6 +184,7 @@ func updateXEPG(background bool) {
createXMLTVFile() createXMLTVFile()
createM3UFile() createM3UFile()
queuePlexGuideRefresh("xepg update")
showInfo("XEPG:" + fmt.Sprintf("Ready to use")) showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
System.ScanInProgress = 0 System.ScanInProgress = 0
@@ -207,9 +213,9 @@ func updateXEPG(background bool) {
func createXEPGMapping() { func createXEPGMapping() {
Data.XMLTV.Files = getLocalProviderFiles("xmltv") 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 friendlyDisplayName = func(channel Channel) (displayName string) {
var dn = channel.DisplayName var dn = channel.DisplayName
@@ -250,10 +256,10 @@ func createXEPGMapping() {
if err == nil { if err == nil {
// Daten aus der XML Datei in eine temporäre Map schreiben // 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 { for _, c := range xmltv.Channel {
var channel = make(map[string]interface{}) var channel = make(map[string]any)
channel["id"] = c.ID channel["id"] = c.ID
channel["display-name"] = friendlyDisplayName(*c) channel["display-name"] = friendlyDisplayName(*c)
@@ -271,7 +277,7 @@ func createXEPGMapping() {
} }
Data.XMLTV.Mapping = tmpMap Data.XMLTV.Mapping = tmpMap
tmpMap = make(map[string]interface{}) tmpMap = make(map[string]any)
} else { } else {
@@ -282,7 +288,7 @@ func createXEPGMapping() {
} }
// Auswahl für den Dummy erstellen // 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"} var times = []string{"30", "60", "90", "120", "180", "240", "360"}
for _, i := range times { for _, i := range times {
@@ -306,7 +312,7 @@ func createXEPGDatabase() (err error) {
var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit) var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit)
Data.Cache.Streams.Active = make([]string, 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) Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG)
if err != nil { if err != nil {
@@ -521,10 +527,132 @@ func createXEPGDatabase() (err error) {
return 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 // Kanäle automatisch zuordnen und das Mapping überprüfen
func mapping() (err error) { func mapping() (err error) {
showInfo("XEPG:" + "Map channels") 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 { for xepg, dxc := range Data.XEPG.Channels {
var xepgChannel XEPGChannelStruct var xepgChannel XEPGChannelStruct
@@ -537,7 +665,7 @@ func mapping() (err error) {
if xepgChannel.XActive == false { if xepgChannel.XActive == false {
// Werte kann "-" sein, deswegen len < 1 // 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 var tvgID = xepgChannel.TvgID
@@ -549,16 +677,16 @@ func mapping() (err error) {
for file, xmltvChannels := range Data.XMLTV.Mapping { 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.XmltvFile = file
xepgChannel.XMapping = channelID xepgChannel.XMapping = channelID
xepgChannel.XActive = true xepgChannel.XActive = true
// Falls in der XMLTV Datei ein Logo existiert, wird dieses verwendet. Falls nicht, dann das Logo aus der M3U Datei // 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 { if len(icon) > 0 {
xepgChannel.TvgLogo = icon xepgChannel.TvgLogo = icon
} }
@@ -585,9 +713,9 @@ func mapping() (err error) {
if file != "xTeVe Dummy" { 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 // Kanallogo aktualisieren
if logo, ok := channel["icon"].(string); ok { if logo, ok := channel["icon"].(string); ok {
@@ -599,20 +727,68 @@ func mapping() (err error) {
} }
} else { } else {
if channelID, replacementChannel, remapOK := findXEPGReplacementChannel(value, xepgChannel); remapOK {
xepgChannel.XMapping = channelID
ShowError(fmt.Errorf(fmt.Sprintf("Missing EPG data: %s", xepgChannel.Name)), 0) if logo, ok := replacementChannel["icon"].(string); ok {
showWarning(2302) 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 xepgChannel.XActive = false
} else {
relaxedKeepCount++
appendXEPGIssueSample(relaxedKeepSamples, name)
}
}
} }
} else { } else {
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file))) 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) if strictMissingEPGMode == true {
showWarning(2301) missingXMLTVCount++
appendXEPGIssueSample(missingXMLTVSamples, providerName)
xepgChannel.XActive = false 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) err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
if err != nil { if err != nil {
return return
@@ -667,7 +865,7 @@ func createXMLTVFile() (err error) {
} }
if len(Data.XMLTV.Files) == 0 && len(Data.Streams.Active) == 0 { 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 return
} }
@@ -735,6 +933,13 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile
var channelID = xepgChannel.XMapping 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 var xmltv XMLTV
@@ -744,6 +949,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
err = getLocalXMLTV(xmltvFile, &xmltv) err = getLocalXMLTV(xmltvFile, &xmltv)
if err != nil { if err != nil {
if relaxedMissingEPGMode == true {
fallbackToDummy()
err = nil
}
return 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 return
} }
@@ -830,7 +1043,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
var currentDay = currentTime.Format("20060102") var currentDay = currentTime.Format("20060102")
var startTime, _ = time.Parse("20060102150405", currentDay+"000000") 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, "_") var dl = strings.Split(xepgChannel.XMapping, "_")
dummyLength, err := strconv.Atoi(dl[0]) dummyLength, err := strconv.Atoi(dl[0])
@@ -839,7 +1052,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
return return
} }
for d := 0; d < 4; d++ { for d := range 4 {
var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24)) var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24))

View File

@@ -16,6 +16,8 @@ function login() {
if (value.length == 0) { if (value.length == 0) {
inputs[i].style.borderColor = "red" inputs[i].style.borderColor = "red"
err = true err = true
} else {
inputs[i].style.borderColor = ""
} }
data[key] = value data[key] = value
@@ -30,7 +32,6 @@ function login() {
if (data.hasOwnProperty("confirm")) { if (data.hasOwnProperty("confirm")) {
if (data["confirm"] != data["password"]) { if (data["confirm"] != data["password"]) {
alert("sdafsd")
document.getElementById('password').style.borderColor = "red" document.getElementById('password').style.borderColor = "red"
document.getElementById('confirm').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 UNDO = new Object()
var SERVER_CONNECTION = false var SERVER_CONNECTION = false
var WS_AVAILABLE = false var WS_AVAILABLE = false
var ACTIVE_MENU_ID:string = ""
var LAST_BULK_CHECKBOX:HTMLInputElement = null
// Menü // Menü
@@ -21,7 +23,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// Kategorien für die Einstellungen // Kategorien für die Einstellungen
var settingsCategory = new Array() 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.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.backup}}", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api")) 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; 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) { function changeButtonAction(element, buttonID, attribute) {
@@ -144,6 +183,59 @@ function getAllSelectedChannels():string[] {
return channels 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() { function selectAllChannels() {
var bulk:Boolean = false var bulk:Boolean = false
@@ -173,6 +265,7 @@ function selectAllChannels() {
} }
LAST_BULK_CHECKBOX = null
return return
} }
@@ -197,6 +290,7 @@ function bulkEdit() {
(rows[i] as HTMLInputElement).checked = false (rows[i] as HTMLInputElement).checked = false
} }
LAST_BULK_CHECKBOX = null
return return
} }
@@ -379,17 +473,19 @@ function searchInMapping() {
function calculateWrapperHeight() { function calculateWrapperHeight() {
if (document.getElementById("box-wrapper")){
var elm = document.getElementById("box-wrapper"); var elm = document.getElementById("box-wrapper");
var content = document.getElementById("content");
var divs = new Array("myStreamsBox", "clientInfo", "content"); if (elm != null && content != null){
var elementsHeight = 0 - elm.offsetHeight;
for (var i = 0; i < divs.length; i++) { var contentTop = content.getBoundingClientRect().top
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight; 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 logs = SERVER["log"]["log"]
var div = document.getElementById("content_log") var div = document.getElementById("content_log")
var wrapper = document.getElementById("box-wrapper") as HTMLElement
var shouldStickToBottom:boolean = bottom
div.innerHTML = "" 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) var keys = getObjKeys(logs)
keys.forEach(logID => { keys.forEach(logID => {
@@ -44,9 +53,8 @@ function showLogs(bottom:boolean) {
setTimeout(function(){ setTimeout(function(){
if (bottom == true) { if (shouldStickToBottom == true && wrapper != null) {
var wrapper = document.getElementById("box-wrapper");
wrapper.scrollTop = wrapper.scrollHeight; wrapper.scrollTop = wrapper.scrollHeight;
} }

View File

@@ -37,6 +37,7 @@ class MainMenuItem extends MainMenu {
var item = document.createElement("LI") var item = document.createElement("LI")
item.setAttribute("onclick", "javascript: openThisMenu(this)") item.setAttribute("onclick", "javascript: openThisMenu(this)")
item.setAttribute("id", this.id) item.setAttribute("id", this.id)
item.setAttribute("data-menu", this.menuKey)
var img = this.createIMG(this.imgSrc) var img = this.createIMG(this.imgSrc)
var value = this.createValue(this.value) var value = this.createValue(this.value)
@@ -64,7 +65,7 @@ class MainMenuItem extends MainMenu {
break break
case "mapping": 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 break
} }
@@ -424,11 +425,7 @@ class Content {
cell.child = true cell.child = true
cell.childType = "IMG" cell.childType = "IMG"
cell.imageURL = data[key]["tvg-logo"] cell.imageURL = data[key]["tvg-logo"]
var td = cell.createCell() tr.appendChild(cell.createCell())
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
// Kanalname // Kanalname
var cell:Cell = new Cell() var cell:Cell = new Cell()
@@ -436,10 +433,7 @@ class Content {
cell.childType = "P" cell.childType = "P"
cell.className = data[key]["x-category"] cell.className = data[key]["x-category"]
cell.value = data[key]["x-name"] cell.value = data[key]["x-name"]
var td = cell.createCell() tr.appendChild(cell.createCell())
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
// Playlist // Playlist
@@ -448,10 +442,7 @@ class Content {
cell.childType = "P" cell.childType = "P"
//cell.value = data[key]["_file.m3u.name"] //cell.value = data[key]["_file.m3u.name"]
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name") cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name")
var td = cell.createCell() tr.appendChild(cell.createCell())
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
// Gruppe (group-title) // Gruppe (group-title)
@@ -459,10 +450,7 @@ class Content {
cell.child = true cell.child = true
cell.childType = "P" cell.childType = "P"
cell.value = data[key]["x-group-title"] cell.value = data[key]["x-group-title"]
var td = cell.createCell() tr.appendChild(cell.createCell())
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
// XMLTV Datei // XMLTV Datei
var cell:Cell = new Cell() var cell:Cell = new Cell()
@@ -475,10 +463,7 @@ class Content {
cell.value = data[key]["x-xmltv-file"] cell.value = data[key]["x-xmltv-file"]
} }
var td = cell.createCell() tr.appendChild(cell.createCell())
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
tr.appendChild(td)
// XMLTV Kanal // XMLTV Kanal
var cell:Cell = new Cell() var cell:Cell = new Cell()
@@ -490,11 +475,18 @@ class Content {
value = data[key]["x-mapping"].substring(0, 20) + "..." value = data[key]["x-mapping"].substring(0, 20) + "..."
} }
cell.value = value cell.value = value
var td = cell.createCell() tr.appendChild(cell.createCell())
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
td.id = key
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) rows.push(tr)
}); });
@@ -561,6 +553,9 @@ class Cell {
(element as HTMLInputElement).checked = this.value; (element as HTMLInputElement).checked = this.value;
(element as HTMLInputElement).type = "checkbox"; (element as HTMLInputElement).type = "checkbox";
(element as HTMLInputElement).className = "bulk hideBulk"; (element as HTMLInputElement).className = "bulk hideBulk";
(element as HTMLInputElement).addEventListener("click", function(event) {
scheduleChannelRangeSelection((element as HTMLInputElement), (event as MouseEvent))
})
break break
case "BULK_HEAD": case "BULK_HEAD":
@@ -578,6 +573,14 @@ class Cell {
element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''" ) element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''" )
//onerror="this.onerror=null;this.src='missing.gif';" //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) td.appendChild(element)
@@ -683,7 +686,7 @@ class ShowContent extends Content {
input.setAttribute("id", "searchMapping") input.setAttribute("id", "searchMapping")
input.setAttribute("placeholder", "{{.button.search}}") input.setAttribute("placeholder", "{{.button.search}}")
input.className = "search" input.className = "search"
input.setAttribute("onchange", 'javascript: searchInMapping()') input.setAttribute("oninput", 'javascript: searchInMapping()')
interaction.appendChild(input) interaction.appendChild(input)
break; 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() { function PageReady() {
initShellLayout()
var server:Server = new Server("getServerConfig") var server:Server = new Server("getServerConfig")
server.request(new Object()) 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(){ window.addEventListener("resize", function(){
if (window.innerWidth > 900) {
setLayoutMenuState(false)
}
calculateWrapperHeight(); calculateWrapperHeight();
}, true); }, true);
setInterval(function(){ setInterval(function(){
if (shouldPollLogs() == false) {
return
}
updateLog() updateLog()
}, 10000); }, 10000);
@@ -841,6 +1055,35 @@ function PageReady() {
return 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() { function createLayout() {
// Client Info // Client Info
@@ -848,11 +1091,10 @@ function createLayout() {
var keys = getObjKeys(obj); var keys = getObjKeys(obj);
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
if (document.getElementById(keys[i])) { setClientInfoValue(keys[i], obj[keys[i]])
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
}
} }
renderStatusCards()
if (!document.getElementById("main-menu")) { if (!document.getElementById("main-menu")) {
return 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 return
} }
function openThisMenu(element) { function openThisMenu(element) {
var id = element.id var id = element.id
var content:ShowContent = new ShowContent(id) var content:ShowContent = new ShowContent(id)
setActiveMenu(id)
content.show() content.show()
closeLayoutMenuIfMobile()
calculateWrapperHeight() calculateWrapperHeight()
return return

View File

@@ -18,6 +18,7 @@ class Server {
if (this.cmd != "updateLog") { if (this.cmd != "updateLog") {
showElement("loading", true) showElement("loading", true)
UNDO = new Object() UNDO = new Object()
setConnectionState("busy")
} }
switch(window.location.protocol) { switch(window.location.protocol) {
@@ -29,13 +30,67 @@ class Server {
break 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 data["cmd"] = this.cmd
var requestCmd:string = data["cmd"]
var ws = new WebSocket(url) 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.onopen = function() {
WS_AVAILABLE = true WS_AVAILABLE = true
if (data["cmd"] != "updateLog") {
setConnectionState("busy")
}
console.log("REQUEST (JS):"); console.log("REQUEST (JS):");
console.log(data) console.log(data)
@@ -50,9 +105,13 @@ class Server {
ws.onerror = function(e) { ws.onerror = function(e) {
console.log("No websocket connection to xTeVe could be established. Check your network configuration.") 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.") alert("No websocket connection to xTeVe could be established. Check your network configuration.")
} }
@@ -60,9 +119,8 @@ class Server {
ws.onmessage = function (e) { ws.onmessage = function (e) {
responseReceived = true
SERVER_CONNECTION = false finishRequest("online", true)
showElement("loading", false)
console.log("RESPONSE:"); console.log("RESPONSE:");
var response = JSON.parse(e.data); var response = JSON.parse(e.data);
@@ -74,6 +132,7 @@ class Server {
} }
if (response["status"] == false) { if (response["status"] == false) {
setConnectionState("offline")
alert(response["err"]) 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) { function getCookie(name) {
var value = "; " + document.cookie; var value = "; " + document.cookie;
var parts = value.split("; " + name + "="); var parts = value.split("; " + name + "=");

View File

@@ -75,6 +75,35 @@ class SettingsCategory {
setting.appendChild(tdRight) setting.appendChild(tdRight)
break 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": case "buffer.timeout":
var tdLeft = document.createElement("TD") var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":" tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"
@@ -276,6 +305,20 @@ class SettingsCategory {
var tdLeft = document.createElement("TD") var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.api.title}}" + ":" tdLeft.innerHTML = "{{.settings.api.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "use_plexAPI":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":"
var tdRight = document.createElement("TD") var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey) var input = content.createCheckbox(settingsKey)
input.checked = data input.checked = data
@@ -287,6 +330,22 @@ class SettingsCategory {
break break
// Select // 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": case "tuner":
var tdLeft = document.createElement("TD") var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":" tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
@@ -454,6 +513,14 @@ class SettingsCategory {
text = "{{.settings.userAgent.description}}" text = "{{.settings.userAgent.description}}"
break break
case "plex.url":
text = "{{.settings.plexURL.description}}"
break
case "plex.token":
text = "{{.settings.plexToken.description}}"
break
case "ffmpeg.path": case "ffmpeg.path":
text = "{{.settings.ffmpegPath.description}}" text = "{{.settings.ffmpegPath.description}}"
break break
@@ -486,6 +553,10 @@ class SettingsCategory {
text = "{{.settings.api.description}}" text = "{{.settings.api.description}}"
break break
case "use_plexAPI":
text = "{{.settings.usePlexAPI.description}}"
break
case "files.update": case "files.update":
text = "{{.settings.filesUpdate.description}}" text = "{{.settings.filesUpdate.description}}"
break break
@@ -498,6 +569,10 @@ class SettingsCategory {
text = "{{.settings.replaceEmptyImages.description}}" text = "{{.settings.replaceEmptyImages.description}}"
break break
case "xepg.missing.epg.mode":
text = "{{.settings.xepgMissingEPGMode.description}}"
break
case "udpxy": case "udpxy":
text = "{{.settings.udpxy.description}}" text = "{{.settings.udpxy.description}}"
break break

View File

@@ -39,7 +39,8 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
const Name = "xTeVe" const Name = "xTeVe"
// Version : Version, die Build Nummer wird in der main func geparst. // 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 // DBVersion : Datanbank Version
const DBVersion = "2.1.0" const DBVersion = "2.1.0"