Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f558a855ae | |||
| 125b0bb35f | |||
| 32c3d779c0 | |||
| ce5b12d8b8 | |||
| 76183bfaa2 | |||
| c577d354e7 | |||
| e48a061ca0 | |||
| ffd43d5217 | |||
| 9bd2b32003 | |||
| 8698983bbf | |||
| 60af423335 | |||
| 57b6be74e2 | |||
| c5545cbf08 | |||
| 339d2d0aa5 | |||
| a04b0ede50 | |||
| 84de46a2f2 | |||
| 5eaf5efdb6 | |||
| fb62353de4 | |||
| fa9d41e10c | |||
| e07d4dd878 | |||
| 6053207034 | |||
| 6a8b4bed28 | |||
| 45685ce592 | |||
| 43a9cf5a7e | |||
| a23cc7a183 | |||
| b069d5bee8 | |||
| 8cb9e43a72 |
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
.github
|
||||
.DS_Store
|
||||
|
||||
docker-data
|
||||
|
||||
README-DEV.md
|
||||
|
||||
*.log
|
||||
*.tmp
|
||||
63
.drone.yml
Normal file
63
.drone.yml
Normal 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
52
Dockerfile
Normal 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"]
|
||||
54
README.md
54
README.md
@@ -48,6 +48,59 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
|
||||
|
||||
---
|
||||
|
||||
## Project Analysis (UI + Operations)
|
||||
|
||||
The core architecture is strong: a Go backend with websocket-driven UI updates, filesystem-based state, and very low runtime overhead.
|
||||
The weakest points are mostly operational and UX-focused:
|
||||
|
||||
* UI was historically utility-first and desktop-biased, with limited responsive behavior and visual hierarchy.
|
||||
* Container usage was documented externally but there was no first-party Dockerfile/compose setup in this repository.
|
||||
* Static web assets are generated into `src/webUI.go`, which works, but creates large diffs and a heavier edit/build cycle.
|
||||
|
||||
### Recommended next technical improvements
|
||||
|
||||
1. Replace generated `src/webUI.go` with Go `embed` for simpler static asset management and cleaner PR diffs.
|
||||
2. Add CI checks (`go test ./...`, build on Linux/arm64/amd64, docker build smoke test).
|
||||
3. Add a dedicated health endpoint (for example `/healthz`) to decouple health checks from HDHomeRun endpoints.
|
||||
4. Add integration tests around websocket commands that mutate settings/files to reduce regression risk.
|
||||
|
||||
---
|
||||
|
||||
## Container-First Run (Included In This Repo)
|
||||
|
||||
### Build image
|
||||
```bash
|
||||
docker build -t xteve:local .
|
||||
```
|
||||
|
||||
### Run with Docker Compose (bridge mode)
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Compose file: `docker-compose.yml`
|
||||
Persistent config volume: `./docker-data/config:/xteve/config`
|
||||
|
||||
### Run with Docker Compose (host networking, Linux recommended for discovery)
|
||||
```bash
|
||||
docker compose -f docker-compose.host.yml up -d
|
||||
```
|
||||
|
||||
Host networking improves LAN discovery behavior (SSDP/DLNA) for Plex/Emby in many setups.
|
||||
|
||||
### Container environment variables
|
||||
|
||||
* `XTEVE_CONFIG` (default: `/xteve/config`)
|
||||
* `XTEVE_PORT` (default: `34400`)
|
||||
|
||||
### Image details
|
||||
|
||||
* Multi-stage build (Go builder + minimal Alpine runtime)
|
||||
* Runs as non-root user (`xteve`)
|
||||
* Built-in healthcheck against `http://127.0.0.1:${XTEVE_PORT}/lineup_status.json`
|
||||
|
||||
---
|
||||
|
||||
## Downloads v2 | 64 Bit only
|
||||
#### 64 Bit Intel / AMD
|
||||
|
||||
@@ -156,4 +209,3 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
#### 2.2.0.0201-beta
|
||||
Add UI enhancement for range selecting checkboxes
|
||||
|
||||
#### 2.2.0.0200-beta
|
||||
```diff
|
||||
+ Major web UI redesign focused on cleaner layout, better information hierarchy, and faster daily workflows.
|
||||
+ Large mobile UX overhaul: responsive navigation, improved spacing, touch-friendly controls, and better small-screen mapping/config flows.
|
||||
+ Accessibility pass across auth, configuration, and main app screens (focus visibility, keyboard flow, ARIA/announcer updates, contrast and status feedback improvements).
|
||||
+ Settings UX improvements and additional polish in frontend behavior for menu, configuration, and authentication interactions.
|
||||
+ Added optional Plex API refresh integration with new settings: use_plexAPI, plex.url, plex.token.
|
||||
+ Added debounced/queued Plex DVR guide reload workflow after lineup and XEPG updates to reduce manual refresh work in Plex.
|
||||
+ Container/runtime improvements and validation updates for easier, safer container usage.
|
||||
+ Added Drone CI pipeline for tests/build checks, Docker/Compose validation, and Docker image publishing to registry.coadcorp.com via plugins/docker.
|
||||
+ Pipeline build version is now derived from changelog-beta.md and validated against source version to prevent release/version drift.
|
||||
```
|
||||
|
||||
#### 2.1.1.0116-beta
|
||||
If no user agent is specified, the default FFmpeg or VLC user agent is used.
|
||||
|
||||
|
||||
15
cmd/webui-gen/main.go
Normal file
15
cmd/webui-gen/main.go
Normal 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
18
docker-compose.host.yml
Normal 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
20
docker-compose.yml
Normal 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
97
docker/entrypoint.sh
Executable 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
13
go.mod
@@ -1,9 +1,14 @@
|
||||
module xteve
|
||||
|
||||
go 1.16
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
github.com/koron/go-ssdp v0.0.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/koron/go-ssdp v0.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
)
|
||||
|
||||
12
go.sum
12
go.sum
@@ -1,16 +1,28 @@
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/koron/go-ssdp v0.0.2 h1:fL3wAoyT6hXHQlORyXUW4Q23kkQpJRgEAYcZB5BR71o=
|
||||
github.com/koron/go-ssdp v0.0.2/go.mod h1:XoLfkAiA2KeZsYh4DbHxD7h3nR2AZNqVQOa+LJuqPYs=
|
||||
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
|
||||
github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
<script language="javascript" type="text/javascript" src="js/base_ts.js"></script>
|
||||
</head>
|
||||
|
||||
<body onload="javascript: readyForConfiguration(0);">
|
||||
<body class="auth-screen wizard-screen" onload="javascript: readyForConfiguration(0);">
|
||||
|
||||
<div id="loading" class="block">
|
||||
<div id="loading" class="block" role="status" aria-live="polite" aria-label="Loading" aria-hidden="false">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
<div id="box">
|
||||
<main id="box" role="main" aria-labelledby="head-text">
|
||||
|
||||
<table id="clientInfo" class="visible">
|
||||
<table id="clientInfo" class="visible" aria-label="Server information">
|
||||
<tr>
|
||||
<td class="tdKey">Version:</td>
|
||||
<td id="version" class="tdVal"> </td>
|
||||
@@ -46,13 +46,14 @@
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">Configuration</h1>
|
||||
</div>
|
||||
<p id="err" class="errorMsg center"></p>
|
||||
<div id="content">
|
||||
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
|
||||
<div id="content" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Configuration step">
|
||||
|
||||
</div>
|
||||
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
|
||||
<div id="box-footer">
|
||||
<input id="next" class="" type="button" name="next" value="Next" onclick="javascript: saveWizard();">
|
||||
<input id="next" class="" type="button" name="next" value="Next" aria-controls="content" onclick="javascript: saveWizard();">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,38 +10,38 @@
|
||||
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="auth-screen">
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
|
||||
<div id="box">
|
||||
<main id="box" role="main" aria-labelledby="head-text">
|
||||
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">{{.account.headline}}</h1>
|
||||
</div>
|
||||
|
||||
<p id="err" class="errorMsg center"></p>
|
||||
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true"></p>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<form id="authentication" action="" method="post">
|
||||
<form id="authentication" action="" method="post" aria-describedby="err" novalidate>
|
||||
|
||||
<h5>{{.account.username.title}}:</h5>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="">
|
||||
<h5>{{.account.password.title}}:</h5>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="">
|
||||
<h5>{{.account.confirm.title}}:</h5>
|
||||
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value="">
|
||||
<label for="username">{{.account.username.title}}:</label>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
|
||||
<label for="password">{{.account.password.title}}:</label>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="new-password">
|
||||
<label for="confirm">{{.account.confirm.title}}:</label>
|
||||
<input id="confirm" type="password" name="confirm" placeholder="Confirm" value="" autocomplete="new-password">
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="box-footer">
|
||||
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" onclick="javascript: login();">
|
||||
<input id="submit" class="" type="button" value="{{.button.craeteAccount}}" aria-label="{{.button.craeteAccount}}" onclick="javascript: login();">
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
1322
html/css/screen.css
1322
html/css/screen.css
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!---
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
-->
|
||||
<title>xTeVe</title>
|
||||
<link rel="stylesheet" href="css/screen.css" type="text/css">
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
@@ -17,16 +15,19 @@
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="javascript: PageReady();">
|
||||
<body class="app-shell" onload="javascript: PageReady();">
|
||||
|
||||
<div id="loading" class="none">
|
||||
<a class="skip-link" href="#content">Skip to main content</a>
|
||||
|
||||
<div id="loading" class="none" role="status" aria-live="polite" aria-label="Loading" aria-hidden="true">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
<div id="popup" class="none">
|
||||
<div id="popup-custom"></div>
|
||||
<div id="popup" class="none" role="dialog" aria-modal="true" aria-hidden="true" tabindex="-1">
|
||||
<div id="popup-custom" role="document" tabindex="-1"></div>
|
||||
</div>
|
||||
|
||||
<div id="layout-overlay" aria-hidden="true" tabindex="-1"></div>
|
||||
<div id="layout">
|
||||
|
||||
<!--
|
||||
@@ -40,55 +41,62 @@
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div id="menu-wrapper" class="layout-left">
|
||||
<aside id="menu-wrapper" class="layout-left" aria-label="Sidebar menu">
|
||||
<div id= "branch"></div>
|
||||
<div id="logo"></div>
|
||||
<nav id="main-menu"></nav>
|
||||
</div>
|
||||
<nav id="main-menu" role="menubar" aria-label="Main navigation"></nav>
|
||||
</aside>
|
||||
|
||||
<div class="layout-right">
|
||||
<main id="shell-main" class="layout-right">
|
||||
<header id="shell-header">
|
||||
<button id="menu-toggle" type="button" aria-expanded="false" aria-controls="menu-wrapper" aria-label="Toggle navigation menu">Menu</button>
|
||||
<h2 id="shell-title">xTeVe Control Panel</h2>
|
||||
<p id="connection-indicator" class="status-idle" role="status" aria-live="polite" aria-atomic="true">Connecting...</p>
|
||||
</header>
|
||||
|
||||
<table id="clientInfo" class="">
|
||||
<table id="clientInfo" class="" aria-label="Server information">
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">xTeVe:</td>
|
||||
<td id="version" class="tdVal"> </td>
|
||||
<td id="version" class="tdVal" data-label="xTeVe"> </td>
|
||||
<td class="tdKey">OS:</td>
|
||||
<td id="os" class="tdVal"> </td>
|
||||
<td id="os" class="tdVal" data-label="OS"> </td>
|
||||
<td class="tdKey phone">DVR IP:</td>
|
||||
<td id="DVR" class="tdVal phone"> </td>
|
||||
<td id="DVR" class="tdVal phone" data-label="DVR IP"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">UUID:</td>
|
||||
<td id="uuid" class="tdVal"> </td>
|
||||
<td id="uuid" class="tdVal" data-label="UUID"> </td>
|
||||
<td class="tdKey">Arch:</td>
|
||||
<td id="arch" class="tdVal"> </td>
|
||||
<td id="arch" class="tdVal" data-label="Arch"> </td>
|
||||
<td class="tdKey phone">M3U URL:</td>
|
||||
<td id="m3u-url" class="tdVal phone"> </td>
|
||||
<td id="m3u-url" class="tdVal phone" data-label="M3U URL"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">Available Streams:</td>
|
||||
<td id="streams" class="tdVal"> </td>
|
||||
<td id="streams" class="tdVal" data-label="Available Streams"> </td>
|
||||
<td class="tdKey">EPG Source:</td>
|
||||
<td id="epgSource" class="tdVal"> </td>
|
||||
<td id="epgSource" class="tdVal" data-label="EPG Source"> </td>
|
||||
<td class="tdKey phone">XEPG URL:</td>
|
||||
<td id="xepg-url" class="tdVal phone"> </td>
|
||||
<td id="xepg-url" class="tdVal phone" data-label="XEPG URL"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tdKey">XEPG Channels:</td>
|
||||
<td id="xepg" class="tdVal"> </td>
|
||||
<td id="xepg" class="tdVal" data-label="XEPG Channels"> </td>
|
||||
<td class="tdKey">Errors:</td>
|
||||
<td id="errors" class="tdVal"> </td>
|
||||
<td id="errors" class="tdVal" data-label="Errors"> </td>
|
||||
<td class="tdKey">Warnings:</td>
|
||||
<td id="warnings" class="tdVal"> </td>
|
||||
<td id="warnings" class="tdVal" data-label="Warnings"> </td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<div id="myStreamsBox" class="notVisible">
|
||||
<div id="status-cards" class="dashboard-cards" role="list" aria-live="polite" aria-label="System summary"></div>
|
||||
|
||||
<div id="myStreamsBox" class="notVisible" aria-live="polite" aria-label="Stream details">
|
||||
|
||||
<div id="allStreams">
|
||||
<table id="activeStreams"></table>
|
||||
@@ -97,9 +105,10 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div id="content" class=""></div>
|
||||
<div id="content" class="" role="region" aria-live="polite" aria-busy="false" tabindex="-1" aria-label="Main content"></div>
|
||||
<p id="sr-announcer" class="sr-only" aria-live="polite" aria-atomic="true"></p>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
function login() {
|
||||
var err = false;
|
||||
var firstInvalid = null;
|
||||
var data = new Object();
|
||||
var div = document.getElementById("content");
|
||||
var form = document.getElementById("authentication");
|
||||
var errElement = document.getElementById("err");
|
||||
if (errElement != null) {
|
||||
errElement.innerHTML = "";
|
||||
}
|
||||
var inputs = div.getElementsByTagName("INPUT");
|
||||
console.log(inputs);
|
||||
for (var i = inputs.length - 1; i >= 0; i--) {
|
||||
@@ -10,23 +15,54 @@ function login() {
|
||||
var value = inputs[i].value;
|
||||
if (value.length == 0) {
|
||||
inputs[i].style.borderColor = "red";
|
||||
inputs[i].setAttribute("aria-invalid", "true");
|
||||
if (firstInvalid == null) {
|
||||
firstInvalid = inputs[i];
|
||||
}
|
||||
err = true;
|
||||
}
|
||||
else {
|
||||
inputs[i].style.borderColor = "";
|
||||
inputs[i].setAttribute("aria-invalid", "false");
|
||||
}
|
||||
data[key] = value;
|
||||
}
|
||||
if (err == true) {
|
||||
if (firstInvalid != null) {
|
||||
firstInvalid.focus();
|
||||
}
|
||||
data = new Object();
|
||||
return;
|
||||
}
|
||||
if (data.hasOwnProperty("confirm")) {
|
||||
if (data["confirm"] != data["password"]) {
|
||||
alert("sdafsd");
|
||||
document.getElementById('password').style.borderColor = "red";
|
||||
document.getElementById('confirm').style.borderColor = "red";
|
||||
document.getElementById('password').setAttribute("aria-invalid", "true");
|
||||
document.getElementById('confirm').setAttribute("aria-invalid", "true");
|
||||
document.getElementById("err").innerHTML = "{{.account.failed}}";
|
||||
document.getElementById('password').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log(data);
|
||||
form.submit();
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var form = document.getElementById("authentication");
|
||||
if (form == null) {
|
||||
return;
|
||||
}
|
||||
var inputs = form.getElementsByTagName("INPUT");
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
inputs[i].addEventListener("keydown", function (event) {
|
||||
if (event.key == "Enter") {
|
||||
event.preventDefault();
|
||||
login();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (inputs.length > 0) {
|
||||
inputs[0].focus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@ var SEARCH_MAPPING = new Object();
|
||||
var UNDO = new Object();
|
||||
var SERVER_CONNECTION = false;
|
||||
var WS_AVAILABLE = false;
|
||||
// Menü
|
||||
var ACTIVE_MENU_ID = "";
|
||||
var LAST_FOCUSED_ELEMENT = null;
|
||||
var LAST_BULK_CHECKBOX = null;
|
||||
// Menu
|
||||
var menuItems = new Array();
|
||||
menuItems.push(new MainMenuItem("playlist", "{{.mainMenu.item.playlist}}", "m3u.png", "{{.mainMenu.headline.playlist}}"));
|
||||
//menuItems.push(new MainMenuItem("pmsID", "{{.mainMenu.item.pmsID}}", "number.png", "{{.mainMenu.headline.pmsID}}"))
|
||||
@@ -18,8 +21,8 @@ menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.
|
||||
menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"));
|
||||
// Kategorien für die Einstellungen
|
||||
var settingsCategory = new Array();
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"));
|
||||
@@ -44,7 +47,74 @@ function showElement(elmID, type) {
|
||||
cssClass = "none";
|
||||
break;
|
||||
}
|
||||
document.getElementById(elmID).className = cssClass;
|
||||
var element = document.getElementById(elmID);
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
element.className = cssClass;
|
||||
element.setAttribute("aria-hidden", type == true ? "false" : "true");
|
||||
if (elmID == "loading" && document.body != null) {
|
||||
document.body.setAttribute("aria-busy", type == true ? "true" : "false");
|
||||
}
|
||||
if (elmID == "popup") {
|
||||
var popupContent_1 = document.getElementById("popup-custom");
|
||||
if (type == true) {
|
||||
LAST_FOCUSED_ELEMENT = document.activeElement;
|
||||
if (popupContent_1 != null) {
|
||||
setTimeout(function () {
|
||||
popupContent_1.focus();
|
||||
}, 20);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (LAST_FOCUSED_ELEMENT != null && LAST_FOCUSED_ELEMENT.focus != undefined) {
|
||||
setTimeout(function () {
|
||||
LAST_FOCUSED_ELEMENT.focus();
|
||||
}, 20);
|
||||
}
|
||||
LAST_FOCUSED_ELEMENT = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
function announceToScreenReader(message) {
|
||||
if (message == undefined || message.length == 0) {
|
||||
return;
|
||||
}
|
||||
var region = document.getElementById("sr-announcer");
|
||||
if (region == null) {
|
||||
return;
|
||||
}
|
||||
region.innerText = "";
|
||||
setTimeout(function () {
|
||||
region.innerText = message;
|
||||
}, 20);
|
||||
}
|
||||
function setConnectionState(state, text) {
|
||||
var label = text;
|
||||
if (label == undefined || label.length == 0) {
|
||||
switch (state) {
|
||||
case "online":
|
||||
label = "Connected";
|
||||
break;
|
||||
case "busy":
|
||||
label = "Syncing";
|
||||
break;
|
||||
case "offline":
|
||||
label = "Offline";
|
||||
break;
|
||||
default:
|
||||
label = "Connecting";
|
||||
break;
|
||||
}
|
||||
}
|
||||
var indicator = document.getElementById("connection-indicator");
|
||||
if (indicator == null) {
|
||||
return;
|
||||
}
|
||||
indicator.className = "status-" + state;
|
||||
indicator.innerText = label;
|
||||
indicator.setAttribute("aria-label", "Connection status: " + label);
|
||||
announceToScreenReader("Connection status " + label);
|
||||
}
|
||||
function changeButtonAction(element, buttonID, attribute) {
|
||||
var value = element.options[element.selectedIndex].value;
|
||||
@@ -114,6 +184,48 @@ function getAllSelectedChannels() {
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
function scheduleChannelRangeSelection(checkbox, event) {
|
||||
var shiftPressed = false;
|
||||
if (event != undefined && event.shiftKey == true) {
|
||||
shiftPressed = true;
|
||||
}
|
||||
// Run after the native checkbox toggle so we copy the final checked state.
|
||||
setTimeout(function () {
|
||||
selectChannelRange(checkbox, shiftPressed);
|
||||
}, 0);
|
||||
}
|
||||
function selectChannelRange(checkbox, shiftPressed) {
|
||||
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
|
||||
return;
|
||||
}
|
||||
var table = document.getElementById("content_table");
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
var trs = table.getElementsByTagName("TR");
|
||||
var visibleCheckboxes = new Array();
|
||||
for (var i = 1; i < trs.length; i++) {
|
||||
if (trs[i].style.display != "none") {
|
||||
var bulkCheckbox = trs[i].querySelector("input.bulk");
|
||||
if (bulkCheckbox != null) {
|
||||
visibleCheckboxes.push(bulkCheckbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
var currentIndex = visibleCheckboxes.indexOf(checkbox);
|
||||
var previousIndex = -1;
|
||||
if (LAST_BULK_CHECKBOX != null) {
|
||||
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX);
|
||||
}
|
||||
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
|
||||
var start = Math.min(previousIndex, currentIndex);
|
||||
var end = Math.max(previousIndex, currentIndex);
|
||||
for (var i = start; i <= end; i++) {
|
||||
visibleCheckboxes[i].checked = checkbox.checked;
|
||||
}
|
||||
}
|
||||
LAST_BULK_CHECKBOX = checkbox;
|
||||
}
|
||||
function selectAllChannels() {
|
||||
var bulk = false;
|
||||
var trs = document.getElementById("content_table").getElementsByTagName("TR");
|
||||
@@ -132,6 +244,7 @@ function selectAllChannels() {
|
||||
}
|
||||
}
|
||||
}
|
||||
LAST_BULK_CHECKBOX = null;
|
||||
return;
|
||||
}
|
||||
function bulkEdit() {
|
||||
@@ -150,6 +263,7 @@ function bulkEdit() {
|
||||
rows[i].className = className;
|
||||
rows[i].checked = false;
|
||||
}
|
||||
LAST_BULK_CHECKBOX = null;
|
||||
return;
|
||||
}
|
||||
function sortTable(column) {
|
||||
@@ -160,15 +274,29 @@ function sortTable(column) {
|
||||
var table = document.getElementById("content_table");
|
||||
var tableHead = table.getElementsByTagName("TR")[0];
|
||||
var tableItems = tableHead.getElementsByTagName("TD");
|
||||
for (var h = 0; h < tableItems.length; h++) {
|
||||
if (tableItems[h].getAttribute("role") == "columnheader") {
|
||||
tableItems[h].setAttribute("aria-sort", "none");
|
||||
}
|
||||
}
|
||||
var sortObj = new Object();
|
||||
var x, xValue;
|
||||
var tableHeader;
|
||||
var sortByString = false;
|
||||
if (column > 0 && COLUMN_TO_SORT > 0) {
|
||||
tableItems[COLUMN_TO_SORT].className = "pointer";
|
||||
tableItems[column].className = "sortThis";
|
||||
tableItems[COLUMN_TO_SORT].classList.remove("sortThis");
|
||||
tableItems[COLUMN_TO_SORT].classList.add("pointer");
|
||||
tableItems[column].classList.remove("pointer");
|
||||
tableItems[column].classList.add("sortThis");
|
||||
}
|
||||
COLUMN_TO_SORT = column;
|
||||
var mobileSort = document.getElementById("mapping-sort-mobile");
|
||||
if (mobileSort != null && (column == 1 || column == 3 || column == 4 || column == 5)) {
|
||||
mobileSort.value = column.toString();
|
||||
}
|
||||
if (tableItems[column] != undefined && tableItems[column].getAttribute("role") == "columnheader") {
|
||||
tableItems[column].setAttribute("aria-sort", "ascending");
|
||||
}
|
||||
var rows = table.rows;
|
||||
if (rows[1] != undefined) {
|
||||
tableHeader = rows[0];
|
||||
@@ -228,6 +356,7 @@ function createSearchObj() {
|
||||
var channels = getObjKeys(data);
|
||||
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"];
|
||||
channels.forEach(function (id) {
|
||||
SEARCH_MAPPING[id] = "";
|
||||
channelKeys.forEach(function (key) {
|
||||
if (key == "x-active") {
|
||||
switch (data[id][key]) {
|
||||
@@ -260,6 +389,9 @@ function searchInMapping() {
|
||||
for (var i = 1; i < trs.length; ++i) {
|
||||
var id = trs[i].getAttribute("id");
|
||||
var element = SEARCH_MAPPING[id];
|
||||
if (element == undefined) {
|
||||
continue;
|
||||
}
|
||||
switch (element.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
case true:
|
||||
document.getElementById(id).style.display = "";
|
||||
@@ -269,17 +401,19 @@ function searchInMapping() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
announceToScreenReader("Search updated");
|
||||
return;
|
||||
}
|
||||
function calculateWrapperHeight() {
|
||||
if (document.getElementById("box-wrapper")) {
|
||||
var elm = document.getElementById("box-wrapper");
|
||||
var divs = new Array("myStreamsBox", "clientInfo", "content");
|
||||
var elementsHeight = 0 - elm.offsetHeight;
|
||||
for (var i = 0; i < divs.length; i++) {
|
||||
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
|
||||
var elm = document.getElementById("box-wrapper");
|
||||
var content = document.getElementById("content");
|
||||
if (elm != null && content != null) {
|
||||
var contentTop = content.getBoundingClientRect().top;
|
||||
var freeSpace = window.innerHeight - contentTop - 26;
|
||||
if (freeSpace < 180) {
|
||||
freeSpace = 180;
|
||||
}
|
||||
elm.style.height = window.innerHeight - elementsHeight + "px";
|
||||
elm.style.height = freeSpace + "px";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ var WizardItem = /** @class */ (function (_super) {
|
||||
var key = this.key;
|
||||
var content = new PopupContent();
|
||||
var description;
|
||||
var wizardField = null;
|
||||
var doc = document.getElementById(this.DocumentID);
|
||||
doc.setAttribute("aria-busy", "true");
|
||||
doc.innerHTML = "";
|
||||
doc.appendChild(headline);
|
||||
switch (key) {
|
||||
@@ -50,6 +52,7 @@ var WizardItem = /** @class */ (function (_super) {
|
||||
select.setAttribute("class", "wizard");
|
||||
select.id = key;
|
||||
doc.appendChild(select);
|
||||
wizardField = select;
|
||||
description = "{{.wizard.tuner.description}}";
|
||||
break;
|
||||
case "epgSource":
|
||||
@@ -59,6 +62,7 @@ var WizardItem = /** @class */ (function (_super) {
|
||||
select.setAttribute("class", "wizard");
|
||||
select.id = key;
|
||||
doc.appendChild(select);
|
||||
wizardField = select;
|
||||
description = "{{.wizard.epgSource.description}}";
|
||||
break;
|
||||
case "m3u":
|
||||
@@ -67,6 +71,7 @@ var WizardItem = /** @class */ (function (_super) {
|
||||
input.setAttribute("class", "wizard");
|
||||
input.id = key;
|
||||
doc.appendChild(input);
|
||||
wizardField = input;
|
||||
description = "{{.wizard.m3u.description}}";
|
||||
break;
|
||||
case "xmltv":
|
||||
@@ -75,6 +80,7 @@ var WizardItem = /** @class */ (function (_super) {
|
||||
input.setAttribute("class", "wizard");
|
||||
input.id = key;
|
||||
doc.appendChild(input);
|
||||
wizardField = input;
|
||||
description = "{{.wizard.xmltv.description}}";
|
||||
break;
|
||||
default:
|
||||
@@ -82,8 +88,20 @@ var WizardItem = /** @class */ (function (_super) {
|
||||
break;
|
||||
}
|
||||
var pre = document.createElement("PRE");
|
||||
pre.id = "wizard-description-" + key;
|
||||
pre.innerHTML = description;
|
||||
doc.appendChild(pre);
|
||||
if (wizardField != null) {
|
||||
wizardField.setAttribute("aria-label", this.headline);
|
||||
wizardField.setAttribute("aria-describedby", pre.id);
|
||||
setTimeout(function () {
|
||||
wizardField.focus();
|
||||
}, 20);
|
||||
}
|
||||
doc.setAttribute("aria-busy", "false");
|
||||
if (typeof announceToScreenReader == "function") {
|
||||
announceToScreenReader(this.headline + " step");
|
||||
}
|
||||
console.log(headline, key);
|
||||
};
|
||||
return WizardItem;
|
||||
@@ -145,3 +163,20 @@ configurationWizard.push(new WizardItem("tuner", "{{.wizard.tuner.title}}"));
|
||||
configurationWizard.push(new WizardItem("epgSource", "{{.wizard.epgSource.title}}"));
|
||||
configurationWizard.push(new WizardItem("m3u", "{{.wizard.m3u.title}}"));
|
||||
configurationWizard.push(new WizardItem("xmltv", "{{.wizard.xmltv.title}}"));
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var container = document.getElementById("content");
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
container.addEventListener("keydown", function (event) {
|
||||
if (event.key != "Enter") {
|
||||
return;
|
||||
}
|
||||
var target = event.target;
|
||||
if (target == null || target.tagName != "INPUT") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
saveWizard();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,15 +21,22 @@ function showLogs(bottom) {
|
||||
var log = new Log();
|
||||
var logs = SERVER["log"]["log"];
|
||||
var div = document.getElementById("content_log");
|
||||
var wrapper = document.getElementById("box-wrapper");
|
||||
var shouldStickToBottom = bottom;
|
||||
div.innerHTML = "";
|
||||
if (wrapper != null && shouldStickToBottom == false) {
|
||||
var distanceToBottom = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight;
|
||||
if (distanceToBottom < 80) {
|
||||
shouldStickToBottom = true;
|
||||
}
|
||||
}
|
||||
var keys = getObjKeys(logs);
|
||||
keys.forEach(function (logID) {
|
||||
var entry = log.createLog(logs[logID]);
|
||||
div.append(entry);
|
||||
});
|
||||
setTimeout(function () {
|
||||
if (bottom == true) {
|
||||
var wrapper = document.getElementById("box-wrapper");
|
||||
if (shouldStickToBottom == true && wrapper != null) {
|
||||
wrapper.scrollTop = wrapper.scrollHeight;
|
||||
}
|
||||
}, 10);
|
||||
|
||||
@@ -43,7 +43,14 @@ var MainMenuItem = /** @class */ (function (_super) {
|
||||
var item = document.createElement("LI");
|
||||
item.setAttribute("onclick", "javascript: openThisMenu(this)");
|
||||
item.setAttribute("id", this.id);
|
||||
item.setAttribute("data-menu", this.menuKey);
|
||||
item.setAttribute("role", "menuitem");
|
||||
item.setAttribute("tabindex", "0");
|
||||
item.setAttribute("aria-controls", "content");
|
||||
item.setAttribute("aria-label", this.value);
|
||||
item.setAttribute("onkeydown", "if(event.key==='Enter' || event.key===' '){event.preventDefault();openThisMenu(this);}");
|
||||
var img = this.createIMG(this.imgSrc);
|
||||
img.setAttribute("alt", "");
|
||||
var value = this.createValue(this.value);
|
||||
item.appendChild(img);
|
||||
item.appendChild(value);
|
||||
@@ -63,7 +70,7 @@ var MainMenuItem = /** @class */ (function (_super) {
|
||||
this.tableHeader = ["{{.users.table.username}}", "{{.users.table.password}}", "{{.users.table.web}}", "{{.users.table.pms}}", "{{.users.table.m3u}}", "{{.users.table.xml}}", "{{.users.table.api}}"];
|
||||
break;
|
||||
case "mapping":
|
||||
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}"];
|
||||
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}", "{{.mapping.table.edit}}"];
|
||||
break;
|
||||
}
|
||||
//console.log(this.menuKey, this.tableHeader);
|
||||
@@ -356,39 +363,27 @@ var Content = /** @class */ (function () {
|
||||
cell.child = true;
|
||||
cell.childType = "IMG";
|
||||
cell.imageURL = data[key]["tvg-logo"];
|
||||
var td = cell.createCell();
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
td.id = key;
|
||||
tr.appendChild(td);
|
||||
tr.appendChild(cell.createCell());
|
||||
// Kanalname
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
cell.childType = "P";
|
||||
cell.className = data[key]["x-category"];
|
||||
cell.value = data[key]["x-name"];
|
||||
var td = cell.createCell();
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
td.id = key;
|
||||
tr.appendChild(td);
|
||||
tr.appendChild(cell.createCell());
|
||||
// Playlist
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
cell.childType = "P";
|
||||
//cell.value = data[key]["_file.m3u.name"]
|
||||
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name");
|
||||
var td = cell.createCell();
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
td.id = key;
|
||||
tr.appendChild(td);
|
||||
tr.appendChild(cell.createCell());
|
||||
// Gruppe (group-title)
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
cell.childType = "P";
|
||||
cell.value = data[key]["x-group-title"];
|
||||
var td = cell.createCell();
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
td.id = key;
|
||||
tr.appendChild(td);
|
||||
tr.appendChild(cell.createCell());
|
||||
// XMLTV Datei
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
@@ -399,10 +394,7 @@ var Content = /** @class */ (function () {
|
||||
else {
|
||||
cell.value = data[key]["x-xmltv-file"];
|
||||
}
|
||||
var td = cell.createCell();
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
td.id = key;
|
||||
tr.appendChild(td);
|
||||
tr.appendChild(cell.createCell());
|
||||
// XMLTV Kanal
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
@@ -413,10 +405,17 @@ var Content = /** @class */ (function () {
|
||||
value = data[key]["x-mapping"].substring(0, 20) + "...";
|
||||
}
|
||||
cell.value = value;
|
||||
var td = cell.createCell();
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
td.id = key;
|
||||
tr.appendChild(td);
|
||||
tr.appendChild(cell.createCell());
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
cell.childType = "EDIT";
|
||||
cell.value = "{{.button.edit}}";
|
||||
var editTd = cell.createCell();
|
||||
var editButton = editTd.firstChild;
|
||||
editButton.setAttribute('onclick', 'javascript: openPopUp("mapping", this)');
|
||||
editButton.setAttribute("id", key);
|
||||
editButton.setAttribute("aria-label", "Edit " + data[key]["x-name"]);
|
||||
tr.appendChild(editTd);
|
||||
rows.push(tr);
|
||||
});
|
||||
break;
|
||||
@@ -451,7 +450,7 @@ var Cell = /** @class */ (function () {
|
||||
break;
|
||||
case "INPUTCHANNEL":
|
||||
element = document.createElement("INPUT");
|
||||
element.setAttribute("onchange", "javscript: changeChannelNumber(this)");
|
||||
element.setAttribute("onchange", "javascript: changeChannelNumber(this)");
|
||||
element.value = this.value;
|
||||
element.type = "text";
|
||||
break;
|
||||
@@ -460,6 +459,9 @@ var Cell = /** @class */ (function () {
|
||||
element.checked = this.value;
|
||||
element.type = "checkbox";
|
||||
element.className = "bulk hideBulk";
|
||||
element.addEventListener("click", function (event) {
|
||||
scheduleChannelRangeSelection(element, event);
|
||||
});
|
||||
break;
|
||||
case "BULK_HEAD":
|
||||
element = document.createElement("INPUT");
|
||||
@@ -475,6 +477,12 @@ var Cell = /** @class */ (function () {
|
||||
element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''");
|
||||
//onerror="this.onerror=null;this.src='missing.gif';"
|
||||
}
|
||||
break;
|
||||
case "EDIT":
|
||||
element = document.createElement("INPUT");
|
||||
element.type = "button";
|
||||
element.value = this.value;
|
||||
element.className = "mapping-edit-button";
|
||||
}
|
||||
td.appendChild(element);
|
||||
}
|
||||
@@ -483,10 +491,18 @@ var Cell = /** @class */ (function () {
|
||||
}
|
||||
if (this.onclick == true) {
|
||||
td.setAttribute("onclick", this.onclickFunktion);
|
||||
td.className = "pointer";
|
||||
td.className = "pointer keyboard-clickable";
|
||||
td.setAttribute("tabindex", "0");
|
||||
td.setAttribute("role", "button");
|
||||
td.setAttribute("onkeydown", "if(event.key==='Enter' || event.key===' '){event.preventDefault();this.click();}");
|
||||
}
|
||||
if (this.tdClassName != undefined) {
|
||||
td.className = this.tdClassName;
|
||||
if (td.className.length > 0) {
|
||||
td.className = td.className + " " + this.tdClassName;
|
||||
}
|
||||
else {
|
||||
td.className = this.tdClassName;
|
||||
}
|
||||
}
|
||||
return td;
|
||||
};
|
||||
@@ -510,6 +526,7 @@ var ShowContent = /** @class */ (function (_super) {
|
||||
COLUMN_TO_SORT = -1;
|
||||
// Alten Inhalt löschen
|
||||
var doc = document.getElementById(this.DocumentID);
|
||||
doc.setAttribute("aria-busy", "true");
|
||||
doc.innerHTML = "";
|
||||
showPreview(false);
|
||||
// Überschrift
|
||||
@@ -556,11 +573,33 @@ var ShowContent = /** @class */ (function (_super) {
|
||||
var input = this.createInput("button", menuKey, "{{.button.bulkEdit}}");
|
||||
input.setAttribute("onclick", 'javascript: bulkEdit()');
|
||||
interaction.appendChild(input);
|
||||
var sortSelect = document.createElement("SELECT");
|
||||
sortSelect.setAttribute("id", "mapping-sort-mobile");
|
||||
sortSelect.className = "mobile-only-control";
|
||||
sortSelect.setAttribute("aria-label", "Sort mapping");
|
||||
var sortOptions = [
|
||||
{ label: "{{.mapping.table.chNo}}", value: "1" },
|
||||
{ label: "{{.mapping.table.channelName}}", value: "3" },
|
||||
{ label: "{{.mapping.table.playlist}}", value: "4" },
|
||||
{ label: "{{.mapping.table.groupTitle}}", value: "5" }
|
||||
];
|
||||
sortOptions.forEach(function (optionData) {
|
||||
var option = document.createElement("OPTION");
|
||||
option.innerText = optionData.label;
|
||||
option.value = optionData.value;
|
||||
sortSelect.appendChild(option);
|
||||
});
|
||||
sortSelect.value = "1";
|
||||
sortSelect.onchange = function () {
|
||||
sortTable(parseInt(this.value, 10));
|
||||
};
|
||||
interaction.appendChild(sortSelect);
|
||||
var input = this.createInput("search", "search", "");
|
||||
input.setAttribute("id", "searchMapping");
|
||||
input.setAttribute("placeholder", "{{.button.search}}");
|
||||
input.setAttribute("aria-label", "{{.button.search}}");
|
||||
input.className = "search";
|
||||
input.setAttribute("onchange", 'javascript: searchInMapping()');
|
||||
input.setAttribute("oninput", 'javascript: searchInMapping()');
|
||||
interaction.appendChild(input);
|
||||
break;
|
||||
case "settings":
|
||||
@@ -580,6 +619,7 @@ var ShowContent = /** @class */ (function (_super) {
|
||||
var settings = this.createDIV();
|
||||
wrapper.appendChild(settings);
|
||||
showSettings();
|
||||
finalizeContentAccessibility(headline);
|
||||
return;
|
||||
break;
|
||||
case "log":
|
||||
@@ -593,6 +633,7 @@ var ShowContent = /** @class */ (function (_super) {
|
||||
var logs = this.createDIV();
|
||||
wrapper.appendChild(logs);
|
||||
showLogs(true);
|
||||
finalizeContentAccessibility(headline);
|
||||
return;
|
||||
break;
|
||||
case "logout":
|
||||
@@ -665,30 +706,392 @@ var ShowContent = /** @class */ (function (_super) {
|
||||
break;
|
||||
}
|
||||
showElement("loading", false);
|
||||
finalizeContentAccessibility(headline);
|
||||
};
|
||||
return ShowContent;
|
||||
}(Content));
|
||||
var SHELL_LAYOUT_READY = false;
|
||||
function isKeyboardActivationKey(event) {
|
||||
return event.key == "Enter" || event.key == " ";
|
||||
}
|
||||
function makeKeyboardClickable(element, label) {
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
if (element.getAttribute("data-keyboard-ready") == "true") {
|
||||
return;
|
||||
}
|
||||
var tagName = element.tagName.toUpperCase();
|
||||
if (tagName == "INPUT" || tagName == "BUTTON" || tagName == "SELECT" || tagName == "TEXTAREA" || tagName == "A") {
|
||||
return;
|
||||
}
|
||||
element.setAttribute("data-keyboard-ready", "true");
|
||||
if (element.getAttribute("tabindex") == null) {
|
||||
element.setAttribute("tabindex", "0");
|
||||
}
|
||||
if (element.getAttribute("role") == null) {
|
||||
element.setAttribute("role", "button");
|
||||
}
|
||||
element.classList.add("keyboard-clickable");
|
||||
if (label != undefined && label.length > 0) {
|
||||
if (element.getAttribute("aria-label") == null || element.getAttribute("aria-label").length == 0) {
|
||||
element.setAttribute("aria-label", label);
|
||||
}
|
||||
}
|
||||
element.addEventListener("keydown", function (event) {
|
||||
if (isKeyboardActivationKey(event) == false) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.click();
|
||||
});
|
||||
}
|
||||
function applyTableAccessibility(table, sectionName) {
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
table.setAttribute("role", "table");
|
||||
var rows = table.getElementsByTagName("TR");
|
||||
var headerLabels = [];
|
||||
if (rows.length > 0) {
|
||||
var headerCells = rows[0].getElementsByTagName("TD");
|
||||
for (var h = 0; h < headerCells.length; h++) {
|
||||
var headerLabel = headerCells[h].innerText;
|
||||
if (headerLabel == undefined || headerLabel.length == 0) {
|
||||
headerLabel = "Value";
|
||||
}
|
||||
if (headerLabel == "BULK") {
|
||||
headerLabel = "Select";
|
||||
}
|
||||
headerLabels.push(headerLabel);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
rows[i].setAttribute("role", "row");
|
||||
if (rows[i].getAttribute("onclick") != null) {
|
||||
var rowLabel = rows[i].innerText;
|
||||
if (rowLabel == undefined || rowLabel.length == 0) {
|
||||
rowLabel = sectionName + " row";
|
||||
}
|
||||
makeKeyboardClickable(rows[i], rowLabel);
|
||||
}
|
||||
var cells = rows[i].getElementsByTagName("TD");
|
||||
for (var c = 0; c < cells.length; c++) {
|
||||
if (i == 0) {
|
||||
cells[c].setAttribute("role", "columnheader");
|
||||
}
|
||||
else {
|
||||
cells[c].setAttribute("role", "cell");
|
||||
}
|
||||
var dataLabel = headerLabels[c];
|
||||
if (dataLabel == undefined || dataLabel.length == 0) {
|
||||
dataLabel = "Value";
|
||||
}
|
||||
cells[c].setAttribute("data-label", dataLabel);
|
||||
var checkbox = cells[c].querySelector('input[type="checkbox"]');
|
||||
if (checkbox != null) {
|
||||
cells[c].setAttribute("data-cell-type", "checkbox");
|
||||
}
|
||||
else {
|
||||
var image = cells[c].querySelector("img");
|
||||
if (image != null) {
|
||||
cells[c].setAttribute("data-cell-type", "image");
|
||||
}
|
||||
else {
|
||||
var actionButton = cells[c].querySelector("input.mapping-edit-button, button.mapping-edit-button");
|
||||
if (actionButton != null) {
|
||||
cells[c].setAttribute("data-cell-type", "action");
|
||||
}
|
||||
else {
|
||||
cells[c].removeAttribute("data-cell-type");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cells[c].getAttribute("onclick") != null) {
|
||||
var cellLabel = cells[c].innerText;
|
||||
if (cellLabel == undefined || cellLabel.length == 0) {
|
||||
cellLabel = sectionName + " details";
|
||||
}
|
||||
makeKeyboardClickable(cells[c], cellLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function finalizeContentAccessibility(sectionName) {
|
||||
var content = document.getElementById("content");
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
content.setAttribute("aria-busy", "false");
|
||||
var heading = content.getElementsByTagName("H3")[0];
|
||||
if (heading != null) {
|
||||
heading.setAttribute("tabindex", "-1");
|
||||
setTimeout(function () {
|
||||
heading.focus();
|
||||
}, 20);
|
||||
}
|
||||
applyTableAccessibility(document.getElementById("content_table"), sectionName);
|
||||
if (sectionName != undefined && sectionName.length > 0) {
|
||||
announceToScreenReader(sectionName + " loaded");
|
||||
}
|
||||
}
|
||||
function setLayoutMenuState(open) {
|
||||
if (document.body == null) {
|
||||
return;
|
||||
}
|
||||
if (open == true) {
|
||||
document.body.classList.add("menu-open");
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove("menu-open");
|
||||
}
|
||||
var toggle = document.getElementById("menu-toggle");
|
||||
if (toggle != null) {
|
||||
toggle.setAttribute("aria-expanded", open == true ? "true" : "false");
|
||||
toggle.setAttribute("aria-label", open == true ? "Close navigation menu" : "Open navigation menu");
|
||||
}
|
||||
var overlay = document.getElementById("layout-overlay");
|
||||
if (overlay != null) {
|
||||
overlay.setAttribute("aria-hidden", open == true ? "false" : "true");
|
||||
}
|
||||
var wrapper = document.getElementById("menu-wrapper");
|
||||
if (wrapper != null) {
|
||||
if (window.innerWidth <= 900) {
|
||||
wrapper.setAttribute("aria-hidden", open == true ? "false" : "true");
|
||||
}
|
||||
else {
|
||||
wrapper.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
}
|
||||
if (window.innerWidth <= 900 && open == false && toggle != null && wrapper != null && wrapper.contains(document.activeElement)) {
|
||||
toggle.focus();
|
||||
}
|
||||
if (window.innerWidth <= 900 && open == true && wrapper != null) {
|
||||
var firstMenuItem = wrapper.querySelector("#main-menu li");
|
||||
if (firstMenuItem != null) {
|
||||
setTimeout(function () {
|
||||
firstMenuItem.focus();
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
function toggleLayoutMenu() {
|
||||
if (document.body == null) {
|
||||
return;
|
||||
}
|
||||
var isOpen = document.body.classList.contains("menu-open");
|
||||
setLayoutMenuState(!isOpen);
|
||||
}
|
||||
function closeLayoutMenuIfMobile() {
|
||||
if (window.innerWidth <= 900) {
|
||||
setLayoutMenuState(false);
|
||||
}
|
||||
}
|
||||
function setActiveMenu(menuID) {
|
||||
ACTIVE_MENU_ID = menuID.toString();
|
||||
var menu = document.getElementById("main-menu");
|
||||
if (menu == null) {
|
||||
return;
|
||||
}
|
||||
var items = menu.getElementsByTagName("LI");
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
items[i].classList.remove("menu-active");
|
||||
items[i].removeAttribute("aria-current");
|
||||
}
|
||||
var activeItem = document.getElementById(ACTIVE_MENU_ID);
|
||||
var activeMenuKey = "";
|
||||
if (activeItem != null) {
|
||||
activeItem.classList.add("menu-active");
|
||||
activeItem.setAttribute("aria-current", "page");
|
||||
var menuKeyValue = activeItem.getAttribute("data-menu");
|
||||
if (menuKeyValue != null) {
|
||||
activeMenuKey = menuKeyValue;
|
||||
}
|
||||
}
|
||||
if (document.body != null) {
|
||||
if (activeMenuKey.length > 0) {
|
||||
document.body.setAttribute("data-active-menu", activeMenuKey);
|
||||
}
|
||||
else {
|
||||
document.body.removeAttribute("data-active-menu");
|
||||
}
|
||||
if (activeMenuKey == "log") {
|
||||
document.body.classList.add("menu-log-focus");
|
||||
}
|
||||
else {
|
||||
document.body.classList.remove("menu-log-focus");
|
||||
}
|
||||
}
|
||||
}
|
||||
function renderStatusCards() {
|
||||
var wrapper = document.getElementById("status-cards");
|
||||
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
|
||||
return;
|
||||
}
|
||||
var info = SERVER["clientInfo"];
|
||||
var errors = parseInt(info["errors"], 10);
|
||||
var warnings = parseInt(info["warnings"], 10);
|
||||
var cards = [
|
||||
{ label: "Streams", value: info["streams"], tone: "ok" },
|
||||
{ label: "EPG Source", value: info["epgSource"], tone: "neutral" },
|
||||
{ label: "XEPG Channels", value: info["xepg"], tone: "ok" },
|
||||
{ label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok" },
|
||||
{ label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok" },
|
||||
{ label: "DVR", value: info["DVR"], tone: "neutral" }
|
||||
];
|
||||
wrapper.innerHTML = "";
|
||||
cards.forEach(function (card) {
|
||||
var box = document.createElement("DIV");
|
||||
box.className = "status-card status-card-" + card.tone;
|
||||
box.setAttribute("role", "listitem");
|
||||
var label = document.createElement("P");
|
||||
label.className = "status-card-label";
|
||||
label.innerText = card.label;
|
||||
var value = document.createElement("P");
|
||||
value.className = "status-card-value";
|
||||
if (card.value == undefined || card.value == "") {
|
||||
value.innerText = "-";
|
||||
}
|
||||
else {
|
||||
value.innerText = card.value;
|
||||
}
|
||||
box.appendChild(label);
|
||||
box.appendChild(value);
|
||||
box.setAttribute("aria-label", card.label + ": " + value.innerText);
|
||||
wrapper.appendChild(box);
|
||||
});
|
||||
}
|
||||
function initShellLayout() {
|
||||
if (SHELL_LAYOUT_READY == true) {
|
||||
return;
|
||||
}
|
||||
var toggle = document.getElementById("menu-toggle");
|
||||
if (toggle != null) {
|
||||
toggle.onclick = function () {
|
||||
toggleLayoutMenu();
|
||||
};
|
||||
}
|
||||
var overlay = document.getElementById("layout-overlay");
|
||||
if (overlay != null) {
|
||||
overlay.onclick = function () {
|
||||
setLayoutMenuState(false);
|
||||
};
|
||||
}
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key == "Escape") {
|
||||
setLayoutMenuState(false);
|
||||
showElement("popup", false);
|
||||
return;
|
||||
}
|
||||
if (event.key == "/") {
|
||||
var target = event.target;
|
||||
var tagName = target != null && target.tagName != undefined ? target.tagName : "";
|
||||
var onInput = tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT";
|
||||
if (onInput == true) {
|
||||
return;
|
||||
}
|
||||
var search = document.getElementById("searchMapping");
|
||||
if (search != null) {
|
||||
event.preventDefault();
|
||||
search.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
setLayoutMenuState(false);
|
||||
setConnectionState("idle");
|
||||
SHELL_LAYOUT_READY = true;
|
||||
}
|
||||
function shouldPollLogs() {
|
||||
if (document.hidden == true) {
|
||||
return false;
|
||||
}
|
||||
if (document.getElementById("content_log") == null) {
|
||||
return false;
|
||||
}
|
||||
if (ACTIVE_MENU_ID.length < 1) {
|
||||
return false;
|
||||
}
|
||||
var activeItem = document.getElementById(ACTIVE_MENU_ID);
|
||||
if (activeItem == null) {
|
||||
return false;
|
||||
}
|
||||
return activeItem.getAttribute("data-menu") == "log";
|
||||
}
|
||||
function PageReady() {
|
||||
initShellLayout();
|
||||
var server = new Server("getServerConfig");
|
||||
server.request(new Object());
|
||||
var bootstrapAttempts = 0;
|
||||
var maxBootstrapAttempts = 5;
|
||||
var bootstrapTimer = window.setInterval(function () {
|
||||
if (SERVER.hasOwnProperty("clientInfo") == true) {
|
||||
window.clearInterval(bootstrapTimer);
|
||||
return;
|
||||
}
|
||||
if (SERVER_CONNECTION == true) {
|
||||
return;
|
||||
}
|
||||
bootstrapAttempts++;
|
||||
var retryServer = new Server("getServerConfig");
|
||||
retryServer.request(new Object());
|
||||
if (bootstrapAttempts >= maxBootstrapAttempts) {
|
||||
window.clearInterval(bootstrapTimer);
|
||||
}
|
||||
}, 3000);
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.innerWidth > 900) {
|
||||
setLayoutMenuState(false);
|
||||
}
|
||||
calculateWrapperHeight();
|
||||
}, true);
|
||||
setInterval(function () {
|
||||
if (shouldPollLogs() == false) {
|
||||
return;
|
||||
}
|
||||
updateLog();
|
||||
}, 10000);
|
||||
return;
|
||||
}
|
||||
function isClientInfoHttpURL(value) {
|
||||
return /^https?:\/\//i.test(value);
|
||||
}
|
||||
function setClientInfoValue(key, value) {
|
||||
var element = document.getElementById(key);
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
var textValue = "";
|
||||
if (value != undefined && value != null) {
|
||||
textValue = String(value);
|
||||
}
|
||||
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
|
||||
element.innerHTML = "";
|
||||
var anchor = document.createElement("A");
|
||||
anchor.href = textValue;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener noreferrer";
|
||||
anchor.textContent = textValue;
|
||||
element.appendChild(anchor);
|
||||
return;
|
||||
}
|
||||
element.innerHTML = textValue;
|
||||
}
|
||||
function createLayout() {
|
||||
var contentRegion = document.getElementById("content");
|
||||
if (contentRegion != null) {
|
||||
contentRegion.setAttribute("aria-busy", "true");
|
||||
}
|
||||
// Client Info
|
||||
var obj = SERVER["clientInfo"];
|
||||
var keys = getObjKeys(obj);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
if (document.getElementById(keys[i])) {
|
||||
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
|
||||
}
|
||||
setClientInfoValue(keys[i], obj[keys[i]]);
|
||||
}
|
||||
renderStatusCards();
|
||||
if (!document.getElementById("main-menu")) {
|
||||
if (contentRegion != null) {
|
||||
contentRegion.setAttribute("aria-busy", "false");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Menü erstellen
|
||||
@@ -713,12 +1116,35 @@ function createLayout() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
|
||||
setActiveMenu(ACTIVE_MENU_ID);
|
||||
}
|
||||
setLayoutMenuState(document.body.classList.contains("menu-open"));
|
||||
var content = document.getElementById("content");
|
||||
var menu = document.getElementById("main-menu");
|
||||
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
|
||||
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
|
||||
var firstItem = menu.getElementsByTagName("LI")[0];
|
||||
if (firstItem != undefined) {
|
||||
firstItem.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (contentRegion != null) {
|
||||
contentRegion.setAttribute("aria-busy", "false");
|
||||
}
|
||||
return;
|
||||
}
|
||||
function openThisMenu(element) {
|
||||
var id = element.id;
|
||||
var content = new ShowContent(id);
|
||||
setActiveMenu(id);
|
||||
content.show();
|
||||
var contentArea = document.getElementById("content");
|
||||
if (contentArea != null) {
|
||||
contentArea.scrollTop = 0;
|
||||
}
|
||||
closeLayoutMenuIfMobile();
|
||||
calculateWrapperHeight();
|
||||
return;
|
||||
}
|
||||
@@ -756,9 +1182,26 @@ var PopupContent = /** @class */ (function (_super) {
|
||||
}
|
||||
PopupContent.prototype.createHeadline = function (headline) {
|
||||
this.doc.innerHTML = "";
|
||||
var titleBar = document.createElement("DIV");
|
||||
titleBar.className = "popup-title";
|
||||
var element = document.createElement("H3");
|
||||
element.id = "popup-title-text";
|
||||
element.innerHTML = headline.toUpperCase();
|
||||
this.doc.appendChild(element);
|
||||
titleBar.appendChild(element);
|
||||
var closeButton = document.createElement("BUTTON");
|
||||
closeButton.setAttribute("type", "button");
|
||||
closeButton.className = "popup-close";
|
||||
closeButton.setAttribute("aria-label", "Close dialog");
|
||||
closeButton.innerHTML = "×";
|
||||
closeButton.onclick = function () {
|
||||
showElement("popup", false);
|
||||
};
|
||||
titleBar.appendChild(closeButton);
|
||||
this.doc.appendChild(titleBar);
|
||||
var popup = document.getElementById("popup");
|
||||
if (popup != null) {
|
||||
popup.setAttribute("aria-labelledby", "popup-title-text");
|
||||
}
|
||||
// Tabelle erstellen
|
||||
this.table = document.createElement("TABLE");
|
||||
this.doc.appendChild(this.table);
|
||||
|
||||
@@ -11,6 +11,7 @@ var Server = /** @class */ (function () {
|
||||
if (this.cmd != "updateLog") {
|
||||
showElement("loading", true);
|
||||
UNDO = new Object();
|
||||
setConnectionState("busy");
|
||||
}
|
||||
switch (window.location.protocol) {
|
||||
case "http:":
|
||||
@@ -20,11 +21,61 @@ var Server = /** @class */ (function () {
|
||||
this.protocol = "wss://";
|
||||
break;
|
||||
}
|
||||
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token");
|
||||
var wsHost = window.location.host;
|
||||
if (wsHost == undefined || wsHost.length < 1) {
|
||||
wsHost = window.location.hostname;
|
||||
}
|
||||
var url = this.protocol + wsHost + "/data/" + "?Token=" + getCookie("Token");
|
||||
data["cmd"] = this.cmd;
|
||||
var requestCmd = data["cmd"];
|
||||
var ws = new WebSocket(url);
|
||||
var isLogUpdate = data["cmd"] == "updateLog";
|
||||
var responseReceived = false;
|
||||
var requestFinished = false;
|
||||
var timeoutMs = 12000;
|
||||
var requestTimeout;
|
||||
var finishRequest = function (state, responseSuccess) {
|
||||
if (responseSuccess === void 0) { responseSuccess = false; }
|
||||
if (requestFinished == true) {
|
||||
return;
|
||||
}
|
||||
requestFinished = true;
|
||||
SERVER_CONNECTION = false;
|
||||
window.clearTimeout(requestTimeout);
|
||||
if (responseSuccess == true) {
|
||||
if (state == "online") {
|
||||
WS_FAILURE_COUNT = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
WS_FAILURE_COUNT++;
|
||||
}
|
||||
if (isLogUpdate == false) {
|
||||
showElement("loading", false);
|
||||
}
|
||||
if (state != "") {
|
||||
setConnectionState(state);
|
||||
}
|
||||
};
|
||||
requestTimeout = window.setTimeout(function () {
|
||||
console.log("Websocket request timed out.");
|
||||
var timeoutState = "offline";
|
||||
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
|
||||
timeoutState = "idle";
|
||||
}
|
||||
finishRequest(timeoutState, false);
|
||||
try {
|
||||
ws.close();
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, timeoutMs);
|
||||
ws.onopen = function () {
|
||||
WS_AVAILABLE = true;
|
||||
if (data["cmd"] != "updateLog") {
|
||||
setConnectionState("busy");
|
||||
}
|
||||
console.log("REQUEST (JS):");
|
||||
console.log(data);
|
||||
console.log("REQUEST: (JSON)");
|
||||
@@ -33,14 +84,18 @@ var Server = /** @class */ (function () {
|
||||
};
|
||||
ws.onerror = function (e) {
|
||||
console.log("No websocket connection to xTeVe could be established. Check your network configuration.");
|
||||
SERVER_CONNECTION = false;
|
||||
if (WS_AVAILABLE == false) {
|
||||
var errorState = "offline";
|
||||
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
|
||||
errorState = "idle";
|
||||
}
|
||||
finishRequest(errorState, false);
|
||||
if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
|
||||
alert("No websocket connection to xTeVe could be established. Check your network configuration.");
|
||||
}
|
||||
};
|
||||
ws.onmessage = function (e) {
|
||||
SERVER_CONNECTION = false;
|
||||
showElement("loading", false);
|
||||
responseReceived = true;
|
||||
finishRequest("online", true);
|
||||
console.log("RESPONSE:");
|
||||
var response = JSON.parse(e.data);
|
||||
console.log(response);
|
||||
@@ -48,6 +103,7 @@ var Server = /** @class */ (function () {
|
||||
document.cookie = "Token=" + response["token"];
|
||||
}
|
||||
if (response["status"] == false) {
|
||||
setConnectionState("offline");
|
||||
alert(response["err"]);
|
||||
if (response.hasOwnProperty("reload")) {
|
||||
location.reload();
|
||||
@@ -94,9 +150,20 @@ var Server = /** @class */ (function () {
|
||||
}
|
||||
createLayout();
|
||||
};
|
||||
ws.onclose = function () {
|
||||
if (responseReceived == true) {
|
||||
return;
|
||||
}
|
||||
var closeState = "offline";
|
||||
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
|
||||
closeState = "idle";
|
||||
}
|
||||
finishRequest(closeState, false);
|
||||
};
|
||||
};
|
||||
return Server;
|
||||
}());
|
||||
var WS_FAILURE_COUNT = 0;
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
|
||||
@@ -74,6 +74,29 @@ var SettingsCategory = /** @class */ (function () {
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "plex.url":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.plexURL.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "plex.url", data);
|
||||
input.setAttribute("placeholder", "{{.settings.plexURL.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "plex.token":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.plexToken.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("password", "plex.token", data);
|
||||
input.setAttribute("placeholder", "{{.settings.plexToken.placeholder}}");
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "buffer.timeout":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":";
|
||||
@@ -240,7 +263,30 @@ var SettingsCategory = /** @class */ (function () {
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "use_plexAPI":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
// Select
|
||||
case "xepg.missing.epg.mode":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.xepgMissingEPGMode.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var text = ["{{.settings.xepgMissingEPGMode.info_strict}}", "{{.settings.xepgMissingEPGMode.info_relaxed}}"];
|
||||
var values = ["strict", "relaxed"];
|
||||
var select = content.createSelect(text, values, data, settingsKey);
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(select);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "tuner":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":";
|
||||
@@ -364,6 +410,12 @@ var SettingsCategory = /** @class */ (function () {
|
||||
case "user.agent":
|
||||
text = "{{.settings.userAgent.description}}";
|
||||
break;
|
||||
case "plex.url":
|
||||
text = "{{.settings.plexURL.description}}";
|
||||
break;
|
||||
case "plex.token":
|
||||
text = "{{.settings.plexToken.description}}";
|
||||
break;
|
||||
case "ffmpeg.path":
|
||||
text = "{{.settings.ffmpegPath.description}}";
|
||||
break;
|
||||
@@ -388,6 +440,9 @@ var SettingsCategory = /** @class */ (function () {
|
||||
case "api":
|
||||
text = "{{.settings.api.description}}";
|
||||
break;
|
||||
case "use_plexAPI":
|
||||
text = "{{.settings.usePlexAPI.description}}";
|
||||
break;
|
||||
case "files.update":
|
||||
text = "{{.settings.filesUpdate.description}}";
|
||||
break;
|
||||
@@ -397,6 +452,9 @@ var SettingsCategory = /** @class */ (function () {
|
||||
case "xepg.replace.missing.images":
|
||||
text = "{{.settings.replaceEmptyImages.description}}";
|
||||
break;
|
||||
case "xepg.missing.epg.mode":
|
||||
text = "{{.settings.xepgMissingEPGMode.description}}";
|
||||
break;
|
||||
case "udpxy":
|
||||
text = "{{.settings.udpxy.description}}";
|
||||
break;
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"backup": "Backup",
|
||||
"bulkEdit": "Bulk Edit",
|
||||
"cancel": "Cancel",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"done": "Done",
|
||||
"login": "Login",
|
||||
@@ -210,7 +211,8 @@
|
||||
"playlist": "Playlist",
|
||||
"groupTitle": "Group Title",
|
||||
"xmltvFile": "XMLTV File",
|
||||
"xmltvID": "XMLTV ID"
|
||||
"xmltvID": "XMLTV ID",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"active":
|
||||
{
|
||||
@@ -355,6 +357,23 @@
|
||||
"title": "API Interface",
|
||||
"description": "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"
|
||||
},
|
||||
"usePlexAPI":
|
||||
{
|
||||
"title": "Use Plex API Refresh",
|
||||
"description": "When enabled, xTeVe calls Plex directly to refresh DVR guide data after lineup or XEPG updates."
|
||||
},
|
||||
"plexURL":
|
||||
{
|
||||
"title": "Plex Server URL",
|
||||
"description": "Base URL of your Plex server. Example: http://192.168.1.10:32400",
|
||||
"placeholder": "http://plex-host:32400"
|
||||
},
|
||||
"plexToken":
|
||||
{
|
||||
"title": "Plex API Token",
|
||||
"description": "Plex token used for authenticated API calls to refresh your DVR guide.",
|
||||
"placeholder": "Plex X-Plex-Token"
|
||||
},
|
||||
"epgSource":
|
||||
{
|
||||
"title": "EPG Source",
|
||||
@@ -380,6 +399,13 @@
|
||||
"title": "Replace missing program images",
|
||||
"description": "If the poster in the XMLTV program is missing, the channel logo will be used."
|
||||
},
|
||||
"xepgMissingEPGMode":
|
||||
{
|
||||
"title": "Missing EPG Handling",
|
||||
"description": "Strict: channels are deactivated when XMLTV mappings disappear.<br>Relaxed: channels stay active with last-known mappings and fall back to a dummy guide whenever guide data cannot be resolved.",
|
||||
"info_strict": "Strict (deactivate channels)",
|
||||
"info_relaxed": "Relaxed (keep active / dummy guide)"
|
||||
},
|
||||
"xteveAutoUpdate":
|
||||
{
|
||||
"title": "Automatic update of xTeVe",
|
||||
|
||||
@@ -10,36 +10,36 @@
|
||||
<script language="javascript" type="text/javascript" src="js/authentication_ts.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="auth-screen">
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
|
||||
<div id="box">
|
||||
<main id="box" role="main" aria-labelledby="head-text">
|
||||
|
||||
<div id="headline">
|
||||
<h1 id="head-text" class="center">{{.login.headline}}</h1>
|
||||
</div>
|
||||
|
||||
<p id="err" class="errorMsg center">{{.authenticationErr}}</p>
|
||||
<p id="err" class="errorMsg center" role="alert" aria-live="assertive" aria-atomic="true">{{.authenticationErr}}</p>
|
||||
|
||||
<div id="content">
|
||||
|
||||
<form id="authentication" action="" method="post">
|
||||
<form id="authentication" action="" method="post" aria-describedby="err" novalidate>
|
||||
|
||||
<h5>{{.login.username.title}}:</h5>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="">
|
||||
<h5>{{.login.password.title}}:</h5>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="">
|
||||
<label for="username">{{.login.username.title}}:</label>
|
||||
<input id="username" type="text" name="username" placeholder="Username" value="" autocomplete="username">
|
||||
<label for="password">{{.login.password.title}}:</label>
|
||||
<input id="password" type="password" name="password" placeholder="Password" value="" autocomplete="current-password">
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="box-footer">
|
||||
<input id="submit" class="" type="button" value="{{.button.login}}" onclick="javascript: login();">
|
||||
<input id="submit" class="" type="button" value="{{.button.login}}" aria-label="{{.button.login}}" onclick="javascript: login();">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="css/base.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="auth-screen">
|
||||
|
||||
<div id="header" class="imgCenter"></div>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func activatedSystemAuthentication() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var defaults = make(map[string]interface{})
|
||||
var defaults = make(map[string]any)
|
||||
defaults["authentication.web"] = false
|
||||
defaults["authentication.pms"] = false
|
||||
defaults["authentication.xml"] = false
|
||||
@@ -43,7 +43,7 @@ func createFirstUserForAuthentication(username, password string) (token string,
|
||||
token, err = authentication.CheckTheValidityOfTheToken(token)
|
||||
authenticationErr(err)
|
||||
|
||||
var userData = make(map[string]interface{})
|
||||
var userData = make(map[string]any)
|
||||
userData["username"] = username
|
||||
userData["authentication.web"] = true
|
||||
userData["authentication.pms"] = true
|
||||
|
||||
@@ -657,7 +657,7 @@ func connectToStreamingServer(streamID int, playlistID string) {
|
||||
Redirect:
|
||||
|
||||
req, err := http.NewRequest("GET", currentURL, nil)
|
||||
req.Header.Set("User-Agent", Settings.UserAgent)
|
||||
req.Header.Set("User-Agent", getUserAgent())
|
||||
req.Header.Set("Connection", "close")
|
||||
//req.Header.Set("Range", "bytes=0-")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
@@ -1423,14 +1423,16 @@ func thirdPartyBuffer(streamID int, playlistID string) {
|
||||
// User-Agent setzen
|
||||
var args []string
|
||||
|
||||
var userAgent = getUserAgent()
|
||||
|
||||
for i, a := range strings.Split(options, " ") {
|
||||
|
||||
switch bufferType {
|
||||
case "FFMPEG":
|
||||
a = strings.Replace(a, "[URL]", url, -1)
|
||||
if i == 0 {
|
||||
if len(Settings.UserAgent) != 0 {
|
||||
args = []string{"-user_agent", Settings.UserAgent}
|
||||
if len(userAgent) != 0 {
|
||||
args = []string{"-user_agent", userAgent}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1441,8 +1443,8 @@ func thirdPartyBuffer(streamID int, playlistID string) {
|
||||
a = strings.Replace(a, "[URL]", url, -1)
|
||||
args = append(args, a)
|
||||
|
||||
if len(Settings.UserAgent) != 0 {
|
||||
args = append(args, fmt.Sprintf(":http-user-agent=%s", Settings.UserAgent))
|
||||
if len(userAgent) != 0 {
|
||||
args = append(args, fmt.Sprintf(":http-user-agent=%s", userAgent))
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
138
src/data.go
138
src/data.go
@@ -23,6 +23,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
var reloadData = false
|
||||
var cacheImages = false
|
||||
var createXEPGFiles = false
|
||||
var triggerPlexGuideReload = false
|
||||
var debug string
|
||||
|
||||
// -vvv [URL] --sout '#transcode{vcodec=mp4v, acodec=mpga} :standard{access=http, mux=ogg}'
|
||||
@@ -36,6 +37,21 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
case "tuner":
|
||||
showWarning(2105)
|
||||
|
||||
case "use_plexAPI":
|
||||
triggerPlexGuideReload = true
|
||||
|
||||
case "plex.url":
|
||||
if v, ok := value.(string); ok {
|
||||
value = strings.TrimRight(strings.TrimSpace(v), "/")
|
||||
}
|
||||
triggerPlexGuideReload = true
|
||||
|
||||
case "plex.token":
|
||||
if v, ok := value.(string); ok {
|
||||
value = strings.TrimSpace(v)
|
||||
}
|
||||
triggerPlexGuideReload = true
|
||||
|
||||
case "epgSource":
|
||||
reloadData = true
|
||||
|
||||
@@ -43,7 +59,7 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
// Leerzeichen aus den Werten entfernen und Formatierung der Uhrzeit überprüfen (0000 - 2359)
|
||||
var newUpdateTimes = make([]string, 0)
|
||||
|
||||
for _, v := range value.([]interface{}) {
|
||||
for _, v := range value.([]any) {
|
||||
|
||||
v = strings.Replace(v.(string), " ", "", -1)
|
||||
|
||||
@@ -69,6 +85,16 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
case "xepg.replace.missing.images":
|
||||
createXEPGFiles = true
|
||||
|
||||
case "xepg.missing.epg.mode":
|
||||
if v, ok := value.(string); ok {
|
||||
mode := strings.ToLower(strings.TrimSpace(v))
|
||||
if mode != "relaxed" {
|
||||
mode = "strict"
|
||||
}
|
||||
value = mode
|
||||
}
|
||||
reloadData = true
|
||||
|
||||
case "backup.path":
|
||||
value = strings.TrimRight(value.(string), string(os.PathSeparator)) + string(os.PathSeparator)
|
||||
err = checkFolder(value.(string))
|
||||
@@ -119,22 +145,26 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
|
||||
oldSettings[key] = value
|
||||
|
||||
switch fmt.Sprintf("%T", value) {
|
||||
if key == "plex.token" {
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: ******** (%T)", key, value)
|
||||
} else {
|
||||
switch fmt.Sprintf("%T", value) {
|
||||
|
||||
case "bool":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
|
||||
case "bool":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %t (%T)", key, value, value)
|
||||
|
||||
case "string":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
|
||||
case "string":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %s (%T)", key, value, value)
|
||||
|
||||
case "[]interface {}":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
|
||||
case "[]interface {}":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %v (%T)", key, value, value)
|
||||
|
||||
case "float64":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
|
||||
case "float64":
|
||||
debug = fmt.Sprintf("Save Setting:Key: %s | Value: %d (%T)", key, int(value.(float64)), value)
|
||||
|
||||
default:
|
||||
debug = fmt.Sprintf("%T", value)
|
||||
default:
|
||||
debug = fmt.Sprintf("%T", value)
|
||||
}
|
||||
}
|
||||
|
||||
showDebug(debug, 1)
|
||||
@@ -250,6 +280,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
|
||||
}
|
||||
|
||||
if triggerPlexGuideReload == true {
|
||||
queuePlexGuideRefresh("settings change")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
@@ -258,8 +292,8 @@ func updateServerSettings(request RequestStruct) (settings SettingsStruct, err e
|
||||
// Providerdaten speichern (WebUI)
|
||||
func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
var filesMap = make(map[string]interface{})
|
||||
var newData = make(map[string]interface{})
|
||||
var filesMap = make(map[string]any)
|
||||
var newData = make(map[string]any)
|
||||
var indicator string
|
||||
var reloadData = false
|
||||
|
||||
@@ -281,7 +315,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
}
|
||||
|
||||
if len(filesMap) == 0 {
|
||||
filesMap = make(map[string]interface{})
|
||||
filesMap = make(map[string]any)
|
||||
}
|
||||
|
||||
for dataID, data := range newData {
|
||||
@@ -290,15 +324,15 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
// Neue Providerdatei
|
||||
dataID = indicator + randomString(19)
|
||||
data.(map[string]interface{})["new"] = true
|
||||
data.(map[string]any)["new"] = true
|
||||
filesMap[dataID] = data
|
||||
|
||||
} else {
|
||||
|
||||
// Bereits vorhandene Providerdatei
|
||||
for key, value := range data.(map[string]interface{}) {
|
||||
for key, value := range data.(map[string]any) {
|
||||
|
||||
var oldData = filesMap[dataID].(map[string]interface{})
|
||||
var oldData = filesMap[dataID].(map[string]any)
|
||||
oldData[key] = value
|
||||
|
||||
}
|
||||
@@ -319,11 +353,11 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
}
|
||||
|
||||
// Neue Providerdatei
|
||||
if _, ok := data.(map[string]interface{})["new"]; ok {
|
||||
if _, ok := data.(map[string]any)["new"]; ok {
|
||||
|
||||
reloadData = true
|
||||
err = getProviderData(fileType, dataID)
|
||||
delete(data.(map[string]interface{}), "new")
|
||||
delete(data.(map[string]any), "new")
|
||||
|
||||
if err != nil {
|
||||
delete(filesMap, dataID)
|
||||
@@ -332,7 +366,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
}
|
||||
|
||||
if _, ok := data.(map[string]interface{})["delete"]; ok {
|
||||
if _, ok := data.(map[string]any)["delete"]; ok {
|
||||
|
||||
deleteLocalProviderFiles(dataID, fileType)
|
||||
reloadData = true
|
||||
@@ -365,7 +399,7 @@ func saveFiles(request RequestStruct, fileType string) (err error) {
|
||||
// Providerdaten manuell aktualisieren (WebUI)
|
||||
func updateFile(request RequestStruct, fileType string) (err error) {
|
||||
|
||||
var updateData = make(map[string]interface{})
|
||||
var updateData = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
|
||||
@@ -395,7 +429,7 @@ func updateFile(request RequestStruct, fileType string) (err error) {
|
||||
// Providerdaten löschen (WebUI)
|
||||
func deleteLocalProviderFiles(dataID, fileType string) {
|
||||
|
||||
var removeData = make(map[string]interface{})
|
||||
var removeData = make(map[string]any)
|
||||
var fileExtension string
|
||||
|
||||
switch fileType {
|
||||
@@ -424,8 +458,8 @@ func deleteLocalProviderFiles(dataID, fileType string) {
|
||||
// Filtereinstellungen speichern (WebUI)
|
||||
func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
|
||||
|
||||
var filterMap = make(map[int64]interface{})
|
||||
var newData = make(map[int64]interface{})
|
||||
var filterMap = make(map[int64]any)
|
||||
var newData = make(map[int64]any)
|
||||
var defaultFilter FilterStruct
|
||||
var newFilter = false
|
||||
|
||||
@@ -458,15 +492,15 @@ func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
|
||||
}
|
||||
|
||||
// Filter aktualisieren / löschen
|
||||
for key, value := range data.(map[string]interface{}) {
|
||||
for key, value := range data.(map[string]any) {
|
||||
|
||||
// Filter löschen
|
||||
if _, ok := data.(map[string]interface{})["delete"]; ok {
|
||||
if _, ok := data.(map[string]any)["delete"]; ok {
|
||||
delete(filterMap, dataID)
|
||||
break
|
||||
}
|
||||
|
||||
if filter, ok := data.(map[string]interface{})["filter"].(string); ok {
|
||||
if filter, ok := data.(map[string]any)["filter"].(string); ok {
|
||||
|
||||
if len(filter) == 0 {
|
||||
|
||||
@@ -480,7 +514,7 @@ func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
|
||||
|
||||
}
|
||||
|
||||
if oldData, ok := filterMap[dataID].(map[string]interface{}); ok {
|
||||
if oldData, ok := filterMap[dataID].(map[string]any); ok {
|
||||
oldData[key] = value
|
||||
}
|
||||
|
||||
@@ -573,7 +607,7 @@ func saveUserData(request RequestStruct) (err error) {
|
||||
|
||||
var userData = request.UserData
|
||||
|
||||
var newCredentials = func(userID string, newUserData map[string]interface{}) (err error) {
|
||||
var newCredentials = func(userID string, newUserData map[string]any) (err error) {
|
||||
|
||||
var newUsername, newPassword string
|
||||
if username, ok := newUserData["username"].(string); ok {
|
||||
@@ -593,7 +627,7 @@ func saveUserData(request RequestStruct) (err error) {
|
||||
|
||||
for userID, newUserData := range userData {
|
||||
|
||||
err = newCredentials(userID, newUserData.(map[string]interface{}))
|
||||
err = newCredentials(userID, newUserData.(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -603,16 +637,16 @@ func saveUserData(request RequestStruct) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
delete(newUserData.(map[string]interface{}), "password")
|
||||
delete(newUserData.(map[string]interface{}), "confirm")
|
||||
delete(newUserData.(map[string]any), "password")
|
||||
delete(newUserData.(map[string]any), "confirm")
|
||||
|
||||
if _, ok := newUserData.(map[string]interface{})["delete"]; ok {
|
||||
if _, ok := newUserData.(map[string]any)["delete"]; ok {
|
||||
|
||||
authentication.RemoveUser(userID)
|
||||
|
||||
} else {
|
||||
|
||||
err = authentication.WriteUserData(userID, newUserData.(map[string]interface{}))
|
||||
err = authentication.WriteUserData(userID, newUserData.(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -662,11 +696,11 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
|
||||
case "m3u", "xmltv":
|
||||
|
||||
var filesMap = make(map[string]interface{})
|
||||
var data = make(map[string]interface{})
|
||||
var filesMap = make(map[string]any)
|
||||
var data = make(map[string]any)
|
||||
var indicator, dataID string
|
||||
|
||||
filesMap = make(map[string]interface{})
|
||||
filesMap = make(map[string]any)
|
||||
|
||||
data["type"] = key
|
||||
data["new"] = true
|
||||
@@ -737,6 +771,10 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
|
||||
}
|
||||
|
||||
if nextStep == 10 {
|
||||
Settings.WizardCompleted = true
|
||||
}
|
||||
|
||||
err = saveSettings(Settings)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -797,9 +835,9 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
|
||||
Data.Streams.All = make([]interface{}, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.Active = make([]interface{}, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.Inactive = make([]interface{}, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.All = make([]any, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.Active = make([]any, 0, System.UnfilteredChannelLimit)
|
||||
Data.Streams.Inactive = make([]any, 0, System.UnfilteredChannelLimit)
|
||||
Data.Playlist.M3U.Groups.Text = []string{}
|
||||
Data.Playlist.M3U.Groups.Value = []string{}
|
||||
Data.StreamPreviewUI.Active = []string{}
|
||||
@@ -820,7 +858,7 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
for n, i := range playlistFile {
|
||||
|
||||
var channels []interface{}
|
||||
var channels []any
|
||||
var groupTitle, tvgID, uuid int = 0, 0, 0
|
||||
var keys = []string{"group-title", "tvg-id", "uuid"}
|
||||
var compatibility = make(map[string]int)
|
||||
@@ -957,7 +995,7 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.UnfilteredChannelLimit && len(Settings.Filter) == 0 {
|
||||
Data.Streams.Active = Data.Streams.All
|
||||
Data.Streams.Inactive = make([]interface{}, 0)
|
||||
Data.Streams.Inactive = make([]any, 0)
|
||||
|
||||
Data.StreamPreviewUI.Active = Data.StreamPreviewUI.Inactive
|
||||
Data.StreamPreviewUI.Inactive = []string{}
|
||||
@@ -980,6 +1018,10 @@ func buildDatabaseDVR() (err error) {
|
||||
sort.Strings(Data.StreamPreviewUI.Active)
|
||||
sort.Strings(Data.StreamPreviewUI.Inactive)
|
||||
|
||||
if Settings.EpgSource != "XEPG" {
|
||||
queuePlexGuideRefresh("lineup update")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -987,7 +1029,7 @@ func buildDatabaseDVR() (err error) {
|
||||
func getLocalProviderFiles(fileType string) (localFiles []string) {
|
||||
|
||||
var fileExtension string
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
|
||||
@@ -1015,7 +1057,7 @@ func getLocalProviderFiles(fileType string) (localFiles []string) {
|
||||
// Providerparameter anhand von dem Key ausgeben
|
||||
func getProviderParameter(id, fileType, key string) (s string) {
|
||||
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
case "m3u":
|
||||
@@ -1028,7 +1070,7 @@ func getProviderParameter(id, fileType, key string) (s string) {
|
||||
dataMap = Settings.Files.XMLTV
|
||||
}
|
||||
|
||||
if data, ok := dataMap[id].(map[string]interface{}); ok {
|
||||
if data, ok := dataMap[id].(map[string]any); ok {
|
||||
|
||||
if v, ok := data[key].(string); ok {
|
||||
s = v
|
||||
@@ -1046,7 +1088,7 @@ func getProviderParameter(id, fileType, key string) (s string) {
|
||||
// Provider Statistiken Kompatibilität aktualisieren
|
||||
func setProviderCompatibility(id, fileType string, compatibility map[string]int) {
|
||||
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
switch fileType {
|
||||
case "m3u":
|
||||
@@ -1059,7 +1101,7 @@ func setProviderCompatibility(id, fileType string, compatibility map[string]int)
|
||||
dataMap = Settings.Files.XMLTV
|
||||
}
|
||||
|
||||
if data, ok := dataMap[id].(map[string]interface{}); ok {
|
||||
if data, ok := dataMap[id].(map[string]any); ok {
|
||||
|
||||
data["compatibility"] = compatibility
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []interface{}, err error) {
|
||||
func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []any, err error) {
|
||||
|
||||
var hdhrData []interface{}
|
||||
var hdhrData []any
|
||||
|
||||
err = json.Unmarshal(content, &hdhrData)
|
||||
if err == nil {
|
||||
@@ -17,7 +17,7 @@ func makeInteraceFromHDHR(content []byte, playlistName, id string) (channels []i
|
||||
for _, d := range hdhrData {
|
||||
|
||||
var channel = make(map[string]string)
|
||||
var data = d.(map[string]interface{})
|
||||
var data = d.(map[string]any)
|
||||
|
||||
channel["group-title"] = playlistName
|
||||
channel["name"] = data["GuideName"].(string)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var htmlFolder string
|
||||
@@ -16,7 +17,7 @@ var goFile string
|
||||
var mapName string
|
||||
var packageName string
|
||||
|
||||
var blankMap = make(map[string]interface{})
|
||||
var blankMap = make(map[string]any)
|
||||
|
||||
// HTMLInit : Dateipfade festlegen
|
||||
// mapName = Name der zu erstellenden map
|
||||
@@ -68,14 +69,14 @@ func createMapFromFiles(folder string) string {
|
||||
checkErr(err)
|
||||
}
|
||||
|
||||
var content string
|
||||
var content strings.Builder
|
||||
|
||||
for key := range blankMap {
|
||||
var newKey = key
|
||||
content += ` ` + mapName + `["` + newKey + `"` + `] = "` + blankMap[key].(string) + `"` + "\n"
|
||||
content.WriteString(` ` + mapName + `["` + newKey + `"` + `] = "` + blankMap[key].(string) + `"` + "\n")
|
||||
}
|
||||
|
||||
return content
|
||||
return content.String()
|
||||
}
|
||||
|
||||
func readFilesToMap(path string, info os.FileInfo, err error) error {
|
||||
|
||||
133
src/info.go
133
src/info.go
@@ -1,100 +1,101 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ShowSystemInfo : Systeminformationen anzeigen
|
||||
func ShowSystemInfo() {
|
||||
|
||||
fmt.Print("Creating the information takes a moment...")
|
||||
err := buildDatabaseDVR()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
fmt.Print("Creating the information takes a moment...")
|
||||
err := buildDatabaseDVR()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
buildXEPG(false)
|
||||
buildXEPG(false)
|
||||
|
||||
fmt.Println("OK")
|
||||
println()
|
||||
fmt.Println("OK")
|
||||
println()
|
||||
|
||||
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
|
||||
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
|
||||
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
|
||||
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
|
||||
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
|
||||
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
|
||||
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
|
||||
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
|
||||
|
||||
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
|
||||
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
|
||||
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
|
||||
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
|
||||
fmt.Println(fmt.Sprintf("Filter: %d", len(Data.Filter)))
|
||||
fmt.Println(fmt.Sprintf("XEPG Chanels: %d", int(Data.XEPG.XEPGCount)))
|
||||
|
||||
println()
|
||||
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
|
||||
println()
|
||||
fmt.Println(fmt.Sprintf("IPv4 Addresses:"))
|
||||
|
||||
for i, ipv4 := range System.IPAddressesV4 {
|
||||
for i, ipv4 := range System.IPAddressesV4 {
|
||||
|
||||
switch count := i; {
|
||||
switch count := i; {
|
||||
|
||||
case count < 10:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 100:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 10:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 100:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
println()
|
||||
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
|
||||
println()
|
||||
fmt.Println(fmt.Sprintf("IPv6 Addresses:"))
|
||||
|
||||
for i, ipv4 := range System.IPAddressesV6 {
|
||||
for i, ipv4 := range System.IPAddressesV6 {
|
||||
|
||||
switch count := i; {
|
||||
switch count := i; {
|
||||
|
||||
case count < 10:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 100:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 10:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
case count < 100:
|
||||
fmt.Println(fmt.Sprintf(" %d. %s", count, ipv4))
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [General]")
|
||||
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
|
||||
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
|
||||
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
|
||||
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
|
||||
fmt.Println("Settings [General]")
|
||||
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
|
||||
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
|
||||
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
|
||||
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [Files]")
|
||||
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
|
||||
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
|
||||
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
|
||||
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
|
||||
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
|
||||
fmt.Println("Settings [Files]")
|
||||
fmt.Println(fmt.Sprintf("Schedule: %s", strings.Join(Settings.Update, ",")))
|
||||
fmt.Println(fmt.Sprintf("Files Update: %t", Settings.FilesUpdate))
|
||||
fmt.Println(fmt.Sprintf("Folder (tmp): %s", Settings.TempPath))
|
||||
fmt.Println(fmt.Sprintf("Image Chaching: %t", Settings.CacheImages))
|
||||
fmt.Println(fmt.Sprintf("Missing EPG Mode: %s", Settings.XepgMissingEPGMode))
|
||||
fmt.Println(fmt.Sprintf("Replace EPG Image: %t", Settings.XepgReplaceMissingImages))
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [Streaming]")
|
||||
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
|
||||
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
|
||||
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
|
||||
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
|
||||
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
|
||||
fmt.Println("Settings [Streaming]")
|
||||
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
|
||||
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
|
||||
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
|
||||
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
|
||||
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))
|
||||
|
||||
println("---")
|
||||
println("---")
|
||||
|
||||
fmt.Println("Settings [Backup]")
|
||||
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
|
||||
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
|
||||
fmt.Println("Settings [Backup]")
|
||||
fmt.Println(fmt.Sprintf("Folder (backup): %s", Settings.BackupPath))
|
||||
fmt.Println(fmt.Sprintf("Backup Keep: %d", Settings.BackupKeep))
|
||||
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
|
||||
"time"
|
||||
//"fmt"
|
||||
//"log"
|
||||
"time"
|
||||
//"fmt"
|
||||
//"log"
|
||||
)
|
||||
|
||||
const tokenLength = 40
|
||||
@@ -27,19 +28,20 @@ var database string
|
||||
|
||||
var databaseFile = "authentication.json"
|
||||
|
||||
var data = make(map[string]interface{})
|
||||
var tokens = make(map[string]interface{})
|
||||
var data = make(map[string]any)
|
||||
var tokens = make(map[string]any)
|
||||
var tokensMu sync.RWMutex
|
||||
|
||||
var initAuthentication = false
|
||||
|
||||
// Cookie : cookie
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
RawExpires string
|
||||
}
|
||||
|
||||
// Framework examples
|
||||
@@ -128,465 +130,475 @@ func main() {
|
||||
|
||||
// Init : databasePath = Path to authentication.json
|
||||
func Init(databasePath string, validity int) (err error) {
|
||||
database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile
|
||||
database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile
|
||||
|
||||
// Check if the database already exists
|
||||
if _, err = os.Stat(database); os.IsNotExist(err) {
|
||||
// Create an empty database
|
||||
var defaults = make(map[string]interface{})
|
||||
defaults["dbVersion"] = "1.0"
|
||||
defaults["hash"] = "sha256"
|
||||
defaults["users"] = make(map[string]interface{})
|
||||
// Check if the database already exists
|
||||
if _, err = os.Stat(database); os.IsNotExist(err) {
|
||||
// Create an empty database
|
||||
var defaults = make(map[string]any)
|
||||
defaults["dbVersion"] = "1.0"
|
||||
defaults["hash"] = "sha256"
|
||||
defaults["users"] = make(map[string]any)
|
||||
|
||||
if saveDatabase(defaults) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if saveDatabase(defaults) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Loading the database
|
||||
err = loadDatabase()
|
||||
// Loading the database
|
||||
err = loadDatabase()
|
||||
|
||||
// Set Token Validity
|
||||
tokenValidity = validity
|
||||
initAuthentication = true
|
||||
return
|
||||
// Set Token Validity
|
||||
tokenValidity = validity
|
||||
initAuthentication = true
|
||||
return
|
||||
}
|
||||
|
||||
// CreateDefaultUser = created efault user
|
||||
func CreateDefaultUser(username, password string) (err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var users = data["users"].(map[string]interface{})
|
||||
// Check if the default user exists
|
||||
if len(users) > 0 {
|
||||
err = createError(001)
|
||||
return
|
||||
}
|
||||
var users = data["users"].(map[string]any)
|
||||
// Check if the default user exists
|
||||
if len(users) > 0 {
|
||||
err = createError(001)
|
||||
return
|
||||
}
|
||||
|
||||
var defaults = defaultsForNewUser(username, password)
|
||||
users[defaults["_id"].(string)] = defaults
|
||||
saveDatabase(data)
|
||||
var defaults = defaultsForNewUser(username, password)
|
||||
users[defaults["_id"].(string)] = defaults
|
||||
saveDatabase(data)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// CreateNewUser : create new user
|
||||
func CreateNewUser(username, password string) (userID string, err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var checkIfTheUserAlreadyExists = func(username string, userData map[string]interface{}) (err error) {
|
||||
var salt = userData["_salt"].(string)
|
||||
var loginUsername = userData["_username"].(string)
|
||||
var checkIfTheUserAlreadyExists = func(username string, userData map[string]any) (err error) {
|
||||
var salt = userData["_salt"].(string)
|
||||
var loginUsername = userData["_username"].(string)
|
||||
|
||||
if SHA256(username, salt) == loginUsername {
|
||||
err = createError(020)
|
||||
}
|
||||
if SHA256(username, salt) == loginUsername {
|
||||
err = createError(020)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var users = data["users"].(map[string]interface{})
|
||||
for _, userData := range users {
|
||||
err = checkIfTheUserAlreadyExists(username, userData.(map[string]interface{}))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
var users = data["users"].(map[string]any)
|
||||
for _, userData := range users {
|
||||
err = checkIfTheUserAlreadyExists(username, userData.(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var defaults = defaultsForNewUser(username, password)
|
||||
userID = defaults["_id"].(string)
|
||||
users[userID] = defaults
|
||||
var defaults = defaultsForNewUser(username, password)
|
||||
userID = defaults["_id"].(string)
|
||||
users[userID] = defaults
|
||||
|
||||
saveDatabase(data)
|
||||
saveDatabase(data)
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// UserAuthentication : user authentication
|
||||
func UserAuthentication(username, password string) (token string, err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var login = func(username, password string, loginData map[string]interface{}) (err error) {
|
||||
err = createError(010)
|
||||
var login = func(username, password string, loginData map[string]any) (err error) {
|
||||
err = createError(010)
|
||||
|
||||
var salt = loginData["_salt"].(string)
|
||||
var loginUsername = loginData["_username"].(string)
|
||||
var loginPassword = loginData["_password"].(string)
|
||||
var salt = loginData["_salt"].(string)
|
||||
var loginUsername = loginData["_username"].(string)
|
||||
var loginPassword = loginData["_password"].(string)
|
||||
|
||||
if SHA256(username, salt) == loginUsername {
|
||||
if SHA256(password, salt) == loginPassword {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
if SHA256(username, salt) == loginUsername {
|
||||
if SHA256(password, salt) == loginPassword {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var users = data["users"].(map[string]interface{})
|
||||
for id, loginData := range users {
|
||||
err = login(username, password, loginData.(map[string]interface{}))
|
||||
if err == nil {
|
||||
token = setToken(id, "-")
|
||||
return
|
||||
}
|
||||
}
|
||||
var users = data["users"].(map[string]any)
|
||||
for id, loginData := range users {
|
||||
err = login(username, password, loginData.(map[string]any))
|
||||
if err == nil {
|
||||
token = setToken(id, "-")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// CheckTheValidityOfTheToken : check token
|
||||
func CheckTheValidityOfTheToken(token string) (newToken string, err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createError(011)
|
||||
err = createError(011)
|
||||
|
||||
if v, ok := tokens[token]; ok {
|
||||
var expires = v.(map[string]interface{})["expires"].(time.Time)
|
||||
var userID = v.(map[string]interface{})["id"].(string)
|
||||
tokensMu.Lock()
|
||||
defer tokensMu.Unlock()
|
||||
|
||||
if expires.Sub(time.Now().Local()) < 0 {
|
||||
return
|
||||
}
|
||||
if v, ok := tokens[token]; ok {
|
||||
expires := v.(map[string]any)["expires"].(time.Time)
|
||||
|
||||
newToken = setToken(userID, token)
|
||||
if expires.Sub(time.Now().Local()) < 0 {
|
||||
delete(tokens, token)
|
||||
return
|
||||
}
|
||||
|
||||
err = nil
|
||||
// Keep a stable token per session and only refresh expiration.
|
||||
v.(map[string]any)["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
|
||||
newToken = token
|
||||
err = nil
|
||||
}
|
||||
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserID : get user ID
|
||||
func GetUserID(token string) (userID string, err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createError(002)
|
||||
err = createError(002)
|
||||
|
||||
if v, ok := tokens[token]; ok {
|
||||
var expires = v.(map[string]interface{})["expires"].(time.Time)
|
||||
userID = v.(map[string]interface{})["id"].(string)
|
||||
tokensMu.Lock()
|
||||
defer tokensMu.Unlock()
|
||||
|
||||
if expires.Sub(time.Now().Local()) < 0 {
|
||||
return
|
||||
}
|
||||
if v, ok := tokens[token]; ok {
|
||||
expires := v.(map[string]any)["expires"].(time.Time)
|
||||
userID = v.(map[string]any)["id"].(string)
|
||||
|
||||
err = nil
|
||||
}
|
||||
if expires.Sub(time.Now().Local()) < 0 {
|
||||
delete(tokens, token)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
err = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// WriteUserData : save user date
|
||||
func WriteUserData(userID string, userData map[string]interface{}) (err error) {
|
||||
func WriteUserData(userID string, userData map[string]any) (err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createError(030)
|
||||
err = createError(030)
|
||||
|
||||
if v, ok := data["users"].(map[string]interface{})[userID].(map[string]interface{}); ok {
|
||||
if v, ok := data["users"].(map[string]any)[userID].(map[string]any); ok {
|
||||
|
||||
v["data"] = userData
|
||||
err = saveDatabase(data)
|
||||
v["data"] = userData
|
||||
err = saveDatabase(data)
|
||||
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// ReadUserData : load user date
|
||||
func ReadUserData(userID string) (userData map[string]interface{}, err error) {
|
||||
func ReadUserData(userID string) (userData map[string]any, err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createError(031)
|
||||
err = createError(031)
|
||||
|
||||
if v, ok := data["users"].(map[string]interface{})[userID].(map[string]interface{}); ok {
|
||||
userData = v["data"].(map[string]interface{})
|
||||
err = nil
|
||||
if v, ok := data["users"].(map[string]any)[userID].(map[string]any); ok {
|
||||
userData = v["data"].(map[string]any)
|
||||
err = nil
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveUser : remove user
|
||||
func RemoveUser(userID string) (err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createError(032)
|
||||
err = createError(032)
|
||||
|
||||
if _, ok := data["users"].(map[string]interface{})[userID]; ok {
|
||||
if _, ok := data["users"].(map[string]any)[userID]; ok {
|
||||
|
||||
delete(data["users"].(map[string]interface{}), userID)
|
||||
err = saveDatabase(data)
|
||||
delete(data["users"].(map[string]any), userID)
|
||||
err = saveDatabase(data)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// SetDefaultUserData : set default user data
|
||||
func SetDefaultUserData(defaults map[string]interface{}) (err error) {
|
||||
func SetDefaultUserData(defaults map[string]any) (err error) {
|
||||
|
||||
allUserData, err := GetAllUserData()
|
||||
allUserData, err := GetAllUserData()
|
||||
|
||||
for _, d := range allUserData {
|
||||
var data = d.(map[string]interface{})["data"].(map[string]interface{})
|
||||
var userID = d.(map[string]interface{})["_id"].(string)
|
||||
for _, d := range allUserData {
|
||||
var data = d.(map[string]any)["data"].(map[string]any)
|
||||
var userID = d.(map[string]any)["_id"].(string)
|
||||
|
||||
for k, v := range defaults {
|
||||
if _, ok := data[k]; ok {
|
||||
// Key exist
|
||||
} else {
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
err = WriteUserData(userID, data)
|
||||
}
|
||||
return
|
||||
for k, v := range defaults {
|
||||
if _, ok := data[k]; ok {
|
||||
// Key exist
|
||||
} else {
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
err = WriteUserData(userID, data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeCredentials : change credentials
|
||||
func ChangeCredentials(userID, username, password string) (err error) {
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = createError(032)
|
||||
err = createError(032)
|
||||
|
||||
if userData, ok := data["users"].(map[string]interface{})[userID]; ok {
|
||||
//var userData = tmp.(map[string]interface{})
|
||||
var salt = userData.(map[string]interface{})["_salt"].(string)
|
||||
if userData, ok := data["users"].(map[string]any)[userID]; ok {
|
||||
//var userData = tmp.(map[string]interface{})
|
||||
var salt = userData.(map[string]any)["_salt"].(string)
|
||||
|
||||
if len(username) > 0 {
|
||||
userData.(map[string]interface{})["_username"] = SHA256(username, salt)
|
||||
}
|
||||
if len(username) > 0 {
|
||||
userData.(map[string]any)["_username"] = SHA256(username, salt)
|
||||
}
|
||||
|
||||
if len(password) > 0 {
|
||||
userData.(map[string]interface{})["_password"] = SHA256(password, salt)
|
||||
}
|
||||
if len(password) > 0 {
|
||||
userData.(map[string]any)["_password"] = SHA256(password, salt)
|
||||
}
|
||||
|
||||
err = saveDatabase(data)
|
||||
}
|
||||
err = saveDatabase(data)
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// GetAllUserData : get all user data
|
||||
func GetAllUserData() (allUserData map[string]interface{}, err error) {
|
||||
func GetAllUserData() (allUserData map[string]any, err error) {
|
||||
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = checkInit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
var defaults = make(map[string]interface{})
|
||||
defaults["dbVersion"] = "1.0"
|
||||
defaults["hash"] = "sha256"
|
||||
defaults["users"] = make(map[string]interface{})
|
||||
saveDatabase(defaults)
|
||||
data = defaults
|
||||
}
|
||||
if len(data) == 0 {
|
||||
var defaults = make(map[string]any)
|
||||
defaults["dbVersion"] = "1.0"
|
||||
defaults["hash"] = "sha256"
|
||||
defaults["users"] = make(map[string]any)
|
||||
saveDatabase(defaults)
|
||||
data = defaults
|
||||
}
|
||||
|
||||
allUserData = data["users"].(map[string]interface{})
|
||||
return
|
||||
allUserData = data["users"].(map[string]any)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckTheValidityOfTheTokenFromHTTPHeader : get token from HTTP header
|
||||
func CheckTheValidityOfTheTokenFromHTTPHeader(w http.ResponseWriter, r *http.Request) (writer http.ResponseWriter, newToken string, err error) {
|
||||
err = createError(011)
|
||||
for _, cookie := range r.Cookies() {
|
||||
if cookie.Name == "Token" {
|
||||
var token string
|
||||
token, err = CheckTheValidityOfTheToken(cookie.Value)
|
||||
//fmt.Println("T", token, err)
|
||||
writer = SetCookieToken(w, token)
|
||||
newToken = token
|
||||
}
|
||||
}
|
||||
//fmt.Println(err)
|
||||
return
|
||||
err = createError(011)
|
||||
for _, cookie := range r.Cookies() {
|
||||
if cookie.Name == "Token" {
|
||||
var token string
|
||||
token, err = CheckTheValidityOfTheToken(cookie.Value)
|
||||
//fmt.Println("T", token, err)
|
||||
writer = SetCookieToken(w, token)
|
||||
newToken = token
|
||||
}
|
||||
}
|
||||
//fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Framework tools
|
||||
|
||||
func checkInit() (err error) {
|
||||
if initAuthentication == false {
|
||||
err = createError(000)
|
||||
}
|
||||
if initAuthentication == false {
|
||||
err = createError(000)
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func saveDatabase(tmpMap interface{}) (err error) {
|
||||
func saveDatabase(tmpMap any) (err error) {
|
||||
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(database, []byte(jsonString), 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = ioutil.WriteFile(database, []byte(jsonString), 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func loadDatabase() (err error) {
|
||||
jsonString, err := ioutil.ReadFile(database)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
jsonString, err := ioutil.ReadFile(database)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(jsonString), &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(jsonString), &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// SHA256 : password + salt = sha256 string
|
||||
func SHA256(secret, salt string) string {
|
||||
key := []byte(secret)
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write([]byte("_remote_db"))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
key := []byte(secret)
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write([]byte("_remote_db"))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
const alphanum = "-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789aBcDeFgHiJkLmNoPqRsTuVwXyZ_"
|
||||
const alphanum = "-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789aBcDeFgHiJkLmNoPqRsTuVwXyZ_"
|
||||
|
||||
var bytes = make([]byte, n)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
return string(bytes)
|
||||
var bytes = make([]byte, n)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func randomID(n int) string {
|
||||
const alphanum = "ABCDEFGHJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const alphanum = "ABCDEFGHJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var bytes = make([]byte, n)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
return string(bytes)
|
||||
var bytes = make([]byte, n)
|
||||
rand.Read(bytes)
|
||||
for i, b := range bytes {
|
||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func createError(errCode int) (err error) {
|
||||
var errMsg string
|
||||
switch errCode {
|
||||
case 000:
|
||||
errMsg = "Authentication has not yet been initialized"
|
||||
case 001:
|
||||
errMsg = "Default user already exists"
|
||||
case 002:
|
||||
errMsg = "No user id found for this token"
|
||||
case 010:
|
||||
errMsg = "User authentication failed"
|
||||
case 011:
|
||||
errMsg = "Session has expired"
|
||||
case 020:
|
||||
errMsg = "User already exists"
|
||||
case 030:
|
||||
errMsg = "User data could not be saved"
|
||||
case 031:
|
||||
errMsg = "User data could not be read"
|
||||
case 032:
|
||||
errMsg = "User ID was not found"
|
||||
}
|
||||
var errMsg string
|
||||
switch errCode {
|
||||
case 000:
|
||||
errMsg = "Authentication has not yet been initialized"
|
||||
case 001:
|
||||
errMsg = "Default user already exists"
|
||||
case 002:
|
||||
errMsg = "No user id found for this token"
|
||||
case 010:
|
||||
errMsg = "User authentication failed"
|
||||
case 011:
|
||||
errMsg = "Session has expired"
|
||||
case 020:
|
||||
errMsg = "User already exists"
|
||||
case 030:
|
||||
errMsg = "User data could not be saved"
|
||||
case 031:
|
||||
errMsg = "User data could not be read"
|
||||
case 032:
|
||||
errMsg = "User ID was not found"
|
||||
}
|
||||
|
||||
err = errors.New(errMsg)
|
||||
return
|
||||
err = errors.New(errMsg)
|
||||
return
|
||||
}
|
||||
|
||||
func defaultsForNewUser(username, password string) map[string]interface{} {
|
||||
var defaults = make(map[string]interface{})
|
||||
var salt = randomString(saltLength)
|
||||
defaults["_username"] = SHA256(username, salt)
|
||||
defaults["_password"] = SHA256(password, salt)
|
||||
defaults["_salt"] = salt
|
||||
defaults["_id"] = "id-" + randomID(idLength)
|
||||
//defaults["_one.time.token"] = randomString(tokenLength)
|
||||
defaults["data"] = make(map[string]interface{})
|
||||
func defaultsForNewUser(username, password string) map[string]any {
|
||||
var defaults = make(map[string]any)
|
||||
var salt = randomString(saltLength)
|
||||
defaults["_username"] = SHA256(username, salt)
|
||||
defaults["_password"] = SHA256(password, salt)
|
||||
defaults["_salt"] = salt
|
||||
defaults["_id"] = "id-" + randomID(idLength)
|
||||
//defaults["_one.time.token"] = randomString(tokenLength)
|
||||
defaults["data"] = make(map[string]any)
|
||||
|
||||
return defaults
|
||||
return defaults
|
||||
}
|
||||
|
||||
func setToken(id, oldToken string) (newToken string) {
|
||||
delete(tokens, oldToken)
|
||||
tokensMu.Lock()
|
||||
defer tokensMu.Unlock()
|
||||
|
||||
if oldToken != "-" {
|
||||
delete(tokens, oldToken)
|
||||
}
|
||||
|
||||
loopToken:
|
||||
newToken = randomString(tokenLength)
|
||||
if _, ok := tokens[newToken]; ok {
|
||||
goto loopToken
|
||||
}
|
||||
newToken = randomString(tokenLength)
|
||||
if _, ok := tokens[newToken]; ok {
|
||||
goto loopToken
|
||||
}
|
||||
|
||||
var tmp = make(map[string]interface{})
|
||||
tmp["id"] = id
|
||||
tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
|
||||
var tmp = make(map[string]any)
|
||||
tmp["id"] = id
|
||||
tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
|
||||
|
||||
tokens[newToken] = tmp
|
||||
tokens[newToken] = tmp
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func mapToJSON(tmpMap interface{}) string {
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(jsonString)
|
||||
func mapToJSON(tmpMap any) string {
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(jsonString)
|
||||
}
|
||||
|
||||
// SetCookieToken : set cookie
|
||||
func SetCookieToken(w http.ResponseWriter, token string) http.ResponseWriter {
|
||||
expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity))
|
||||
cookie := http.Cookie{Name: "Token", Value: token, Expires: expiration}
|
||||
http.SetCookie(w, &cookie)
|
||||
return w
|
||||
expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity))
|
||||
cookie := http.Cookie{Name: "Token", Value: token, Expires: expiration}
|
||||
http.SetCookie(w, &cookie)
|
||||
return w
|
||||
}
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
package m3u
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type M3UStream struct {
|
||||
GroupTitle string `json:"group-title,required"`
|
||||
Name string `json:"name,required"`
|
||||
TvgID string `json:"tvg-id,required"`
|
||||
TvgLogo string `json:"tvg-logo,required"`
|
||||
TvgName string `json:"tvg-name,required"`
|
||||
URL string `json:"url,required"`
|
||||
UUIDKey string `json:"_uuid.key,omitempty"`
|
||||
UUIDValue string `json:"_uuid.value,omitempty"`
|
||||
GroupTitle string `json:"group-title,required"`
|
||||
Name string `json:"name,required"`
|
||||
TvgID string `json:"tvg-id,required"`
|
||||
TvgLogo string `json:"tvg-logo,required"`
|
||||
TvgName string `json:"tvg-name,required"`
|
||||
URL string `json:"url,required"`
|
||||
UUIDKey string `json:"_uuid.key,omitempty"`
|
||||
UUIDValue string `json:"_uuid.value,omitempty"`
|
||||
}
|
||||
|
||||
func TestStream1(t *testing.T) {
|
||||
|
||||
var file = "test_list_1.m3u"
|
||||
var content, err = ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
var file = "test_list_1.m3u"
|
||||
var content, err = ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
streams, err := MakeInterfaceFromM3U(content)
|
||||
streams, err := MakeInterfaceFromM3U(content)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = checkStream(streams)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkStream(streams)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Println("Streams:", len(streams))
|
||||
t.Log(streams)
|
||||
fmt.Println("Streams:", len(streams))
|
||||
t.Log(streams)
|
||||
|
||||
}
|
||||
|
||||
func checkStream(streamInterface []interface{}) (err error) {
|
||||
func checkStream(streamInterface []any) (err error) {
|
||||
|
||||
for i, s := range streamInterface {
|
||||
for i, s := range streamInterface {
|
||||
|
||||
var stream = s.(map[string]string)
|
||||
var m3uStream M3UStream
|
||||
var stream = s.(map[string]string)
|
||||
var m3uStream M3UStream
|
||||
|
||||
jsonString, err := json.MarshalIndent(stream, "", " ")
|
||||
jsonString, err := json.MarshalIndent(stream, "", " ")
|
||||
|
||||
if err == nil {
|
||||
if err == nil {
|
||||
|
||||
err = json.Unmarshal(jsonString, &m3uStream)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(jsonString, &m3uStream)
|
||||
if err == nil {
|
||||
|
||||
log.Print(fmt.Sprintf("Stream: %d", i))
|
||||
log.Print(fmt.Sprintf("Name*: %s", m3uStream.Name))
|
||||
log.Print(fmt.Sprintf("URL*: %s", m3uStream.URL))
|
||||
log.Print(fmt.Sprintf("tvg-name: %s", m3uStream.TvgName))
|
||||
log.Print(fmt.Sprintf("tvg-id**: %s", m3uStream.TvgID))
|
||||
log.Print(fmt.Sprintf("tvg-logo: %s", m3uStream.TvgLogo))
|
||||
log.Print(fmt.Sprintf("group-title**: %s", m3uStream.GroupTitle))
|
||||
log.Print(fmt.Sprintf("Stream: %d", i))
|
||||
log.Print(fmt.Sprintf("Name*: %s", m3uStream.Name))
|
||||
log.Print(fmt.Sprintf("URL*: %s", m3uStream.URL))
|
||||
log.Print(fmt.Sprintf("tvg-name: %s", m3uStream.TvgName))
|
||||
log.Print(fmt.Sprintf("tvg-id**: %s", m3uStream.TvgID))
|
||||
log.Print(fmt.Sprintf("tvg-logo: %s", m3uStream.TvgLogo))
|
||||
log.Print(fmt.Sprintf("group-title**: %s", m3uStream.GroupTitle))
|
||||
|
||||
if len(m3uStream.UUIDKey) > 0 {
|
||||
log.Print(fmt.Sprintf("UUID key***: %s", m3uStream.UUIDKey))
|
||||
log.Print(fmt.Sprintf("UUID value: %s", m3uStream.UUIDValue))
|
||||
} else {
|
||||
log.Print(fmt.Sprintf("UUID key: false"))
|
||||
}
|
||||
if len(m3uStream.UUIDKey) > 0 {
|
||||
log.Print(fmt.Sprintf("UUID key***: %s", m3uStream.UUIDKey))
|
||||
log.Print(fmt.Sprintf("UUID value: %s", m3uStream.UUIDValue))
|
||||
} else {
|
||||
log.Print(fmt.Sprintf("UUID key: false"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(fmt.Sprintf("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -"))
|
||||
}
|
||||
log.Println(fmt.Sprintf("- - - - - (*: Required) | (**: Nice to have) | (***: Love it) - - - - -"))
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,186 +1,186 @@
|
||||
package m3u
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MakeInterfaceFromM3U :
|
||||
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []interface{}, err error) {
|
||||
func MakeInterfaceFromM3U(byteStream []byte) (allChannels []any, err error) {
|
||||
|
||||
var content = string(byteStream)
|
||||
var channelName string
|
||||
var uuids []string
|
||||
var content = string(byteStream)
|
||||
var channelName string
|
||||
var uuids []string
|
||||
|
||||
var parseMetaData = func(channel string) (stream map[string]string) {
|
||||
var parseMetaData = func(channel string) (stream map[string]string) {
|
||||
|
||||
stream = make(map[string]string)
|
||||
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
|
||||
var exceptForChannelName = `,([^\n]*|,[^\r]*)`
|
||||
stream = make(map[string]string)
|
||||
var exceptForParameter = `[a-z-A-Z=]*(".*?")`
|
||||
var exceptForChannelName = `,([^\n]*|,[^\r]*)`
|
||||
|
||||
var lines = strings.Split(strings.Replace(channel, "\r\n", "\n", -1), "\n")
|
||||
var lines = strings.Split(strings.Replace(channel, "\r\n", "\n", -1), "\n")
|
||||
|
||||
// Zeilen mit # und leerer Zeilen entfernen
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
// Zeilen mit # und leerer Zeilen entfernen
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
|
||||
if len(lines[i]) == 0 || lines[i][0:1] == "#" {
|
||||
lines = append(lines[:i], lines[i+1:]...)
|
||||
}
|
||||
if len(lines[i]) == 0 || lines[i][0:1] == "#" {
|
||||
lines = append(lines[:i], lines[i+1:]...)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) >= 2 {
|
||||
if len(lines) >= 2 {
|
||||
|
||||
for _, line := range lines {
|
||||
for _, line := range lines {
|
||||
|
||||
_, err := url.ParseRequestURI(line)
|
||||
_, err := url.ParseRequestURI(line)
|
||||
|
||||
switch err {
|
||||
switch err {
|
||||
|
||||
case nil:
|
||||
stream["url"] = strings.Trim(line, "\r\n")
|
||||
case nil:
|
||||
stream["url"] = strings.Trim(line, "\r\n")
|
||||
|
||||
default:
|
||||
default:
|
||||
|
||||
var value string
|
||||
// Alle Parameter parsen
|
||||
var p = regexp.MustCompile(exceptForParameter)
|
||||
var streamParameter = p.FindAllString(line, -1)
|
||||
var value string
|
||||
// Alle Parameter parsen
|
||||
var p = regexp.MustCompile(exceptForParameter)
|
||||
var streamParameter = p.FindAllString(line, -1)
|
||||
|
||||
for _, p := range streamParameter {
|
||||
for _, p := range streamParameter {
|
||||
|
||||
line = strings.Replace(line, p, "", 1)
|
||||
line = strings.Replace(line, p, "", 1)
|
||||
|
||||
p = strings.Replace(p, `"`, "", -1)
|
||||
var parameter = strings.SplitN(p, "=", 2)
|
||||
p = strings.Replace(p, `"`, "", -1)
|
||||
var parameter = strings.SplitN(p, "=", 2)
|
||||
|
||||
if len(parameter) == 2 {
|
||||
if len(parameter) == 2 {
|
||||
|
||||
// TVG Key als Kleinbuchstaben speichern
|
||||
switch strings.Contains(parameter[0], "tvg") {
|
||||
// TVG Key als Kleinbuchstaben speichern
|
||||
switch strings.Contains(parameter[0], "tvg") {
|
||||
|
||||
case true:
|
||||
stream[strings.ToLower(parameter[0])] = parameter[1]
|
||||
case false:
|
||||
stream[parameter[0]] = parameter[1]
|
||||
case true:
|
||||
stream[strings.ToLower(parameter[0])] = parameter[1]
|
||||
case false:
|
||||
stream[parameter[0]] = parameter[1]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// URL's nicht an die Filterfunktion übergeben
|
||||
if !strings.Contains(parameter[1], "://") && len(parameter[1]) > 0 {
|
||||
value = value + parameter[1] + " "
|
||||
}
|
||||
// URL's nicht an die Filterfunktion übergeben
|
||||
if !strings.Contains(parameter[1], "://") && len(parameter[1]) > 0 {
|
||||
value = value + parameter[1] + " "
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Kanalnamen parsen
|
||||
n := regexp.MustCompile(exceptForChannelName)
|
||||
var name = n.FindAllString(line, 1)
|
||||
// Kanalnamen parsen
|
||||
n := regexp.MustCompile(exceptForChannelName)
|
||||
var name = n.FindAllString(line, 1)
|
||||
|
||||
if len(name) > 0 {
|
||||
channelName = name[0]
|
||||
channelName = strings.Replace(channelName, `,`, "", 1)
|
||||
channelName = strings.TrimRight(channelName, "\r\n")
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
}
|
||||
if len(name) > 0 {
|
||||
channelName = name[0]
|
||||
channelName = strings.Replace(channelName, `,`, "", 1)
|
||||
channelName = strings.TrimRight(channelName, "\r\n")
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
}
|
||||
|
||||
if len(channelName) == 0 {
|
||||
if len(channelName) == 0 {
|
||||
|
||||
if v, ok := stream["tvg-name"]; ok {
|
||||
channelName = v
|
||||
}
|
||||
if v, ok := stream["tvg-name"]; ok {
|
||||
channelName = v
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
channelName = strings.TrimRight(channelName, " ")
|
||||
|
||||
// Kanäle ohne Namen werden augelassen
|
||||
if len(channelName) == 0 {
|
||||
return
|
||||
}
|
||||
// Kanäle ohne Namen werden augelassen
|
||||
if len(channelName) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
stream["name"] = channelName
|
||||
value = value + channelName
|
||||
stream["name"] = channelName
|
||||
value = value + channelName
|
||||
|
||||
stream["_values"] = value
|
||||
stream["_values"] = value
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Nach eindeutiger ID im Stream suchen
|
||||
for key, value := range stream {
|
||||
// Nach eindeutiger ID im Stream suchen
|
||||
for key, value := range stream {
|
||||
|
||||
if !strings.Contains(strings.ToLower(key), "tvg-id") {
|
||||
if !strings.Contains(strings.ToLower(key), "tvg-id") {
|
||||
|
||||
if strings.Contains(strings.ToLower(key), "id") {
|
||||
if strings.Contains(strings.ToLower(key), "id") {
|
||||
|
||||
if indexOfString(value, uuids) != -1 {
|
||||
log.Println(fmt.Sprintf("Channel: %s - %s = %s ", stream["name"], key, value))
|
||||
break
|
||||
}
|
||||
if indexOfString(value, uuids) != -1 {
|
||||
log.Println(fmt.Sprintf("Channel: %s - %s = %s ", stream["name"], key, value))
|
||||
break
|
||||
}
|
||||
|
||||
uuids = append(uuids, value)
|
||||
uuids = append(uuids, value)
|
||||
|
||||
stream["_uuid.key"] = key
|
||||
stream["_uuid.value"] = value
|
||||
break
|
||||
stream["_uuid.key"] = key
|
||||
stream["_uuid.value"] = value
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//fmt.Println(content)
|
||||
if strings.Contains(content, "#EXT-X-TARGETDURATION") || strings.Contains(content, "#EXT-X-MEDIA-SEQUENCE") {
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
return
|
||||
}
|
||||
//fmt.Println(content)
|
||||
if strings.Contains(content, "#EXT-X-TARGETDURATION") || strings.Contains(content, "#EXT-X-MEDIA-SEQUENCE") {
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(content, "#EXTM3U") {
|
||||
if strings.Contains(content, "#EXTM3U") {
|
||||
|
||||
var channels = strings.Split(content, "#EXTINF")
|
||||
var channels = strings.Split(content, "#EXTINF")
|
||||
|
||||
channels = append(channels[:0], channels[1:]...)
|
||||
channels = append(channels[:0], channels[1:]...)
|
||||
|
||||
for _, channel := range channels {
|
||||
for _, channel := range channels {
|
||||
|
||||
var stream = parseMetaData(channel)
|
||||
var stream = parseMetaData(channel)
|
||||
|
||||
if len(stream) > 0 && stream != nil {
|
||||
allChannels = append(allChannels, stream)
|
||||
}
|
||||
if len(stream) > 0 && stream != nil {
|
||||
allChannels = append(allChannels, stream)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
} else {
|
||||
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
func indexOfString(element string, data []string) int {
|
||||
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return k
|
||||
}
|
||||
}
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ type ClientInfo struct {
|
||||
OS string `json:"os,required"`
|
||||
URL string `json:"url,required"`
|
||||
|
||||
Response ServerResponse `json:"response,omitempty"`
|
||||
Response ServerResponse `json:"response"`
|
||||
}
|
||||
|
||||
//ServerResponse : Response from server after client request
|
||||
// ServerResponse : Response from server after client request
|
||||
type ServerResponse struct {
|
||||
Status bool `json:"status,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
@@ -106,7 +106,7 @@ func serverRequest() (err error) {
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
//fmt.Println(resp.StatusCode, Updater.URL, Updater.CMD)
|
||||
err = fmt.Errorf(fmt.Sprintf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), Updater.URL))
|
||||
err = fmt.Errorf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), Updater.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
// Playlisten parsen
|
||||
func parsePlaylist(filename, fileType string) (channels []interface{}, err error) {
|
||||
func parsePlaylist(filename, fileType string) (channels []any, err error) {
|
||||
|
||||
content, err := readByteFromFile(filename)
|
||||
var id = strings.TrimSuffix(getFilenameFromPath(filename), path.Ext(getFilenameFromPath(filename)))
|
||||
@@ -34,7 +34,7 @@ func parsePlaylist(filename, fileType string) (channels []interface{}, err error
|
||||
}
|
||||
|
||||
// Streams filtern
|
||||
func filterThisStream(s interface{}) (status bool) {
|
||||
func filterThisStream(s any) (status bool) {
|
||||
|
||||
status = false
|
||||
var stream = s.(map[string]string)
|
||||
|
||||
363
src/plex_api.go
Normal file
363
src/plex_api.go
Normal 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
|
||||
}
|
||||
@@ -17,13 +17,13 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
var fileExtension, serverFileName string
|
||||
var body = make([]byte, 0)
|
||||
var newProvider = false
|
||||
var dataMap = make(map[string]interface{})
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
var saveDateFromProvider = func(fileSource, serverFileName, id string, body []byte) (err error) {
|
||||
|
||||
var data = make(map[string]interface{})
|
||||
var data = make(map[string]any)
|
||||
|
||||
if value, ok := dataMap[id].(map[string]interface{}); ok {
|
||||
if value, ok := dataMap[id].(map[string]any); ok {
|
||||
data = value
|
||||
} else {
|
||||
data["id.provider"] = id
|
||||
@@ -65,7 +65,7 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
}
|
||||
|
||||
case "compatibility":
|
||||
data[key] = make(map[string]interface{})
|
||||
data[key] = make(map[string]any)
|
||||
|
||||
case "counter.download":
|
||||
data[key] = 0.0
|
||||
@@ -142,7 +142,7 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
|
||||
for dataID, d := range dataMap {
|
||||
|
||||
var data = d.(map[string]interface{})
|
||||
var data = d.(map[string]any)
|
||||
var fileSource = data["file.source"].(string)
|
||||
newProvider = false
|
||||
|
||||
@@ -220,8 +220,8 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
}
|
||||
|
||||
// Fehler Counter um 1 erhöhen
|
||||
var data = make(map[string]interface{})
|
||||
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
|
||||
var data = make(map[string]any)
|
||||
if value, ok := dataMap[dataID].(map[string]any); ok {
|
||||
|
||||
data = value
|
||||
data["counter.error"] = data["counter.error"].(float64) + 1
|
||||
@@ -238,9 +238,9 @@ func getProviderData(fileType, fileID string) (err error) {
|
||||
// Berechnen der Fehlerquote
|
||||
if newProvider == false {
|
||||
|
||||
if value, ok := dataMap[dataID].(map[string]interface{}); ok {
|
||||
if value, ok := dataMap[dataID].(map[string]any); ok {
|
||||
|
||||
var data = make(map[string]interface{})
|
||||
var data = make(map[string]any)
|
||||
data = value
|
||||
|
||||
if data["counter.error"].(float64) == 0 {
|
||||
@@ -282,15 +282,23 @@ func downloadFileFromServer(providerURL string) (filename string, body []byte, e
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(providerURL)
|
||||
req, err := http.NewRequest(http.MethodGet, providerURL, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp.Header.Set("User-Agent", Settings.UserAgent)
|
||||
req.Header.Set("User-Agent", getUserAgent())
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf(fmt.Sprintf("%d: %s "+http.StatusText(resp.StatusCode), resp.StatusCode, providerURL))
|
||||
err = fmt.Errorf("%d: %s %s", resp.StatusCode, providerURL, http.StatusText(resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
|
||||
// Tuner
|
||||
case 2105:
|
||||
errMsg = fmt.Sprintf("The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again.")
|
||||
errMsg = "The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again."
|
||||
case 2106:
|
||||
errMsg = fmt.Sprintf("This function is only available with XEPG as EPG source")
|
||||
|
||||
|
||||
@@ -47,11 +47,7 @@ type LineupStatus struct {
|
||||
}
|
||||
|
||||
// Lineup : HDHR Lineup /lineup.json
|
||||
type Lineup []interface {
|
||||
//GuideName string `json:"GuideName"`
|
||||
//GuideNumber string `json:"GuideNumber"`
|
||||
//URL string `json:"URL"`
|
||||
}
|
||||
type Lineup []any
|
||||
|
||||
// LineupStream : HDHR einzelner Stream im Lineup
|
||||
type LineupStream struct {
|
||||
|
||||
@@ -149,18 +149,18 @@ type DataStruct struct {
|
||||
}
|
||||
|
||||
Streams struct {
|
||||
Active []interface{}
|
||||
All []interface{}
|
||||
Inactive []interface{}
|
||||
Active []any
|
||||
All []any
|
||||
Inactive []any
|
||||
}
|
||||
|
||||
XMLTV struct {
|
||||
Files []string
|
||||
Mapping map[string]interface{}
|
||||
Mapping map[string]any
|
||||
}
|
||||
|
||||
XEPG struct {
|
||||
Channels map[string]interface{}
|
||||
Channels map[string]any
|
||||
XEPGCount int64
|
||||
}
|
||||
}
|
||||
@@ -254,6 +254,9 @@ type Notification struct {
|
||||
// SettingsStruct : Inhalt der settings.json
|
||||
type SettingsStruct struct {
|
||||
API bool `json:"api"`
|
||||
UsePlexAPI bool `json:"use_plexAPI"`
|
||||
PlexURL string `json:"plex.url"`
|
||||
PlexToken string `json:"plex.token"`
|
||||
AuthenticationAPI bool `json:"authentication.api"`
|
||||
AuthenticationM3U bool `json:"authentication.m3u"`
|
||||
AuthenticationPMS bool `json:"authentication.pms"`
|
||||
@@ -275,30 +278,32 @@ type SettingsStruct struct {
|
||||
FileXMLTV []string `json:"xmltv,omitempty"` // Altes Speichersystem der Provider XML Datei Slice (Wird für die Umwandlung auf das neue benötigt)
|
||||
|
||||
Files struct {
|
||||
HDHR map[string]interface{} `json:"hdhr"`
|
||||
M3U map[string]interface{} `json:"m3u"`
|
||||
XMLTV map[string]interface{} `json:"xmltv"`
|
||||
HDHR map[string]any `json:"hdhr"`
|
||||
M3U map[string]any `json:"m3u"`
|
||||
XMLTV map[string]any `json:"xmltv"`
|
||||
} `json:"files"`
|
||||
|
||||
FilesUpdate bool `json:"files.update"`
|
||||
Filter map[int64]interface{} `json:"filter"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Language string `json:"language"`
|
||||
LogEntriesRAM int `json:"log.entries.ram"`
|
||||
M3U8AdaptiveBandwidthMBPS int `json:"m3u8.adaptive.bandwidth.mbps"`
|
||||
MappingFirstChannel float64 `json:"mapping.first.channel"`
|
||||
Port string `json:"port"`
|
||||
SSDP bool `json:"ssdp"`
|
||||
TempPath string `json:"temp.path"`
|
||||
Tuner int `json:"tuner"`
|
||||
Update []string `json:"update"`
|
||||
UpdateURL string `json:"update.url,omitempty"`
|
||||
UserAgent string `json:"user.agent"`
|
||||
UUID string `json:"uuid"`
|
||||
UDPxy string `json:"udpxy"`
|
||||
Version string `json:"version"`
|
||||
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
|
||||
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
|
||||
FilesUpdate bool `json:"files.update"`
|
||||
Filter map[int64]any `json:"filter"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Language string `json:"language"`
|
||||
LogEntriesRAM int `json:"log.entries.ram"`
|
||||
M3U8AdaptiveBandwidthMBPS int `json:"m3u8.adaptive.bandwidth.mbps"`
|
||||
MappingFirstChannel float64 `json:"mapping.first.channel"`
|
||||
Port string `json:"port"`
|
||||
SSDP bool `json:"ssdp"`
|
||||
TempPath string `json:"temp.path"`
|
||||
Tuner int `json:"tuner"`
|
||||
Update []string `json:"update"`
|
||||
UpdateURL string `json:"update.url,omitempty"`
|
||||
UserAgent string `json:"user.agent"`
|
||||
UUID string `json:"uuid"`
|
||||
UDPxy string `json:"udpxy"`
|
||||
Version string `json:"version"`
|
||||
XepgMissingEPGMode string `json:"xepg.missing.epg.mode"`
|
||||
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
|
||||
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
|
||||
WizardCompleted bool `json:"wizard.completed"`
|
||||
}
|
||||
|
||||
// LanguageUI : Sprache für das WebUI
|
||||
|
||||
@@ -6,11 +6,11 @@ type RequestStruct struct {
|
||||
Cmd string `json:"cmd,required"`
|
||||
|
||||
// Benutzer
|
||||
DeleteUser bool `json:"deleteUser,omitempty"`
|
||||
UserData map[string]interface{} `json:"userData,omitempty"`
|
||||
DeleteUser bool `json:"deleteUser,omitempty"`
|
||||
UserData map[string]any `json:"userData,omitempty"`
|
||||
|
||||
// Mapping
|
||||
EpgMapping map[string]interface{} `json:"epgMapping,omitempty"`
|
||||
EpgMapping map[string]any `json:"epgMapping,omitempty"`
|
||||
|
||||
// Restore
|
||||
Base64 string `json:"base64,omitempty"`
|
||||
@@ -18,6 +18,9 @@ type RequestStruct struct {
|
||||
// Neue Werte für die Einstellungen (settings.json)
|
||||
Settings struct {
|
||||
API *bool `json:"api,omitempty"`
|
||||
UsePlexAPI *bool `json:"use_plexAPI,omitempty"`
|
||||
PlexURL *string `json:"plex.url,omitempty"`
|
||||
PlexToken *string `json:"plex.token,omitempty"`
|
||||
AuthenticationAPI *bool `json:"authentication.api,omitempty"`
|
||||
AuthenticationM3U *bool `json:"authentication.m3u,omitempty"`
|
||||
AuthenticationPMS *bool `json:"authentication.pms,omitempty"`
|
||||
@@ -26,7 +29,7 @@ type RequestStruct struct {
|
||||
BackupKeep *int `json:"backup.keep,omitempty"`
|
||||
BackupPath *string `json:"backup.path,omitempty"`
|
||||
Buffer *string `json:"buffer,omitempty"`
|
||||
BufferSize *int `json:"buffer.size.kb, omitempty"`
|
||||
BufferSize *int `json:"buffer.size.kb,omitempty"`
|
||||
BufferTimeout *float64 `json:"buffer.timeout,omitempty"`
|
||||
CacheImages *bool `json:"cache.images,omitempty"`
|
||||
EpgSource *string `json:"epgSource,omitempty"`
|
||||
@@ -40,24 +43,25 @@ type RequestStruct struct {
|
||||
UDPxy *string `json:"udpxy,omitempty"`
|
||||
Update *[]string `json:"update,omitempty"`
|
||||
UserAgent *string `json:"user.agent,omitempty"`
|
||||
XepgMissingEPGMode *string `json:"xepg.missing.epg.mode,omitempty"`
|
||||
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
|
||||
XteveAutoUpdate *bool `json:"xteveAutoUpdate,omitempty"`
|
||||
SchemeM3U *string `json:"scheme.m3u,omitempty"`
|
||||
SchemeXML *string `json:"scheme.xml,omitempty"`
|
||||
} `json:"settings,omitempty"`
|
||||
} `json:"settings"`
|
||||
|
||||
// Upload Logo
|
||||
Filename string `json:"filename,omitempty"`
|
||||
|
||||
// Filter
|
||||
Filter map[int64]interface{} `json:"filter,omitempty"`
|
||||
Filter map[int64]any `json:"filter,omitempty"`
|
||||
|
||||
// Dateien (M3U, HDHR, XMLTV)
|
||||
Files struct {
|
||||
HDHR map[string]interface{} `json:"hdhr,omitempty"`
|
||||
M3U map[string]interface{} `json:"m3u,omitempty"`
|
||||
XMLTV map[string]interface{} `json:"xmltv,omitempty"`
|
||||
} `json:"files,omitempty"`
|
||||
HDHR map[string]any `json:"hdhr,omitempty"`
|
||||
M3U map[string]any `json:"m3u,omitempty"`
|
||||
XMLTV map[string]any `json:"xmltv,omitempty"`
|
||||
} `json:"files"`
|
||||
|
||||
// Wizard
|
||||
Wizard struct {
|
||||
@@ -65,7 +69,7 @@ type RequestStruct struct {
|
||||
M3U *string `json:"m3u,omitempty"`
|
||||
Tuner *int `json:"tuner,omitempty"`
|
||||
XMLTV *string `json:"xmltv,omitempty"`
|
||||
} `json:"wizard,omitempty"`
|
||||
} `json:"wizard"`
|
||||
}
|
||||
|
||||
// ResponseStruct : Antworten an den Client (WEB)
|
||||
@@ -84,7 +88,7 @@ type ResponseStruct struct {
|
||||
Warnings int `json:"warnings"`
|
||||
XEPGCount int64 `json:"xepg"`
|
||||
XML string `json:"xepg-url,required"`
|
||||
} `json:"clientInfo,omitempty"`
|
||||
} `json:"clientInfo"`
|
||||
|
||||
Data struct {
|
||||
Playlist struct {
|
||||
@@ -102,20 +106,20 @@ type ResponseStruct struct {
|
||||
}
|
||||
} `json:"data,required"`
|
||||
|
||||
Alert string `json:"alert,omitempty"`
|
||||
ConfigurationWizard bool `json:"configurationWizard,required"`
|
||||
Error string `json:"err,omitempty"`
|
||||
Log WebScreenLogStruct `json:"log,required"`
|
||||
LogoURL string `json:"logoURL,omitempty"`
|
||||
OpenLink string `json:"openLink,omitempty"`
|
||||
OpenMenu string `json:"openMenu,omitempty"`
|
||||
Reload bool `json:"reload,omitempty"`
|
||||
Settings SettingsStruct `json:"settings,required"`
|
||||
Status bool `json:"status,required"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Users map[string]interface{} `json:"users,omitempty"`
|
||||
Wizard int `json:"wizard,omitempty"`
|
||||
XEPG map[string]interface{} `json:"xepg,required"`
|
||||
Alert string `json:"alert,omitempty"`
|
||||
ConfigurationWizard bool `json:"configurationWizard,required"`
|
||||
Error string `json:"err,omitempty"`
|
||||
Log WebScreenLogStruct `json:"log,required"`
|
||||
LogoURL string `json:"logoURL,omitempty"`
|
||||
OpenLink string `json:"openLink,omitempty"`
|
||||
OpenMenu string `json:"openMenu,omitempty"`
|
||||
Reload bool `json:"reload,omitempty"`
|
||||
Settings SettingsStruct `json:"settings,required"`
|
||||
Status bool `json:"status,required"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Users map[string]any `json:"users,omitempty"`
|
||||
Wizard int `json:"wizard,omitempty"`
|
||||
XEPG map[string]any `json:"xepg,required"`
|
||||
|
||||
Notification map[string]Notification `json:"notification,omitempty"`
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func createSystemFiles() (err error) {
|
||||
err = checkFile(filename)
|
||||
if err != nil {
|
||||
// Datei existiert nicht, wird jetzt erstellt
|
||||
err = saveMapToJSONFile(filename, make(map[string]interface{}))
|
||||
err = saveMapToJSONFile(filename, make(map[string]any))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -97,15 +97,28 @@ func loadSettings() (settings SettingsStruct, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Deafult Werte setzten
|
||||
var defaults = make(map[string]interface{})
|
||||
var dataMap = make(map[string]interface{})
|
||||
var freshInstall = len(settingsMap) == 0
|
||||
|
||||
dataMap["xmltv"] = make(map[string]interface{})
|
||||
dataMap["m3u"] = make(map[string]interface{})
|
||||
dataMap["hdhr"] = make(map[string]interface{})
|
||||
// Deafult Werte setzten
|
||||
var defaults = make(map[string]any)
|
||||
var dataMap = make(map[string]any)
|
||||
|
||||
dataMap["xmltv"] = make(map[string]any)
|
||||
dataMap["m3u"] = make(map[string]any)
|
||||
dataMap["hdhr"] = make(map[string]any)
|
||||
|
||||
defaultFFmpegPath := ""
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
|
||||
containerFFmpegPath := "/usr/local/bin/ffmpeg"
|
||||
if checkFile(containerFFmpegPath) == nil {
|
||||
defaultFFmpegPath = containerFFmpegPath
|
||||
}
|
||||
}
|
||||
|
||||
defaults["api"] = false
|
||||
defaults["use_plexAPI"] = false
|
||||
defaults["plex.url"] = ""
|
||||
defaults["plex.token"] = ""
|
||||
defaults["authentication.api"] = false
|
||||
defaults["authentication.m3u"] = false
|
||||
defaults["authentication.pms"] = false
|
||||
@@ -118,27 +131,34 @@ func loadSettings() (settings SettingsStruct, err error) {
|
||||
defaults["buffer.timeout"] = 500
|
||||
defaults["cache.images"] = false
|
||||
defaults["epgSource"] = "PMS"
|
||||
defaults["ffmpeg.path"] = defaultFFmpegPath
|
||||
defaults["ffmpeg.options"] = System.FFmpeg.DefaultOptions
|
||||
defaults["vlc.options"] = System.VLC.DefaultOptions
|
||||
defaults["files"] = dataMap
|
||||
defaults["files.update"] = true
|
||||
defaults["filter"] = make(map[string]interface{})
|
||||
defaults["filter"] = make(map[string]any)
|
||||
defaults["git.branch"] = System.Branch
|
||||
defaults["language"] = "en"
|
||||
defaults["log.entries.ram"] = 500
|
||||
defaults["mapping.first.channel"] = 1000
|
||||
defaults["xepg.missing.epg.mode"] = "strict"
|
||||
defaults["xepg.replace.missing.images"] = true
|
||||
defaults["m3u8.adaptive.bandwidth.mbps"] = 10
|
||||
defaults["port"] = "34400"
|
||||
defaults["ssdp"] = true
|
||||
defaults["tuner"] = 1
|
||||
defaults["update"] = []string{"0000"}
|
||||
defaults["user.agent"] = System.Name
|
||||
defaults["user.agent"] = defaultUserAgent
|
||||
defaults["uuid"] = createUUID()
|
||||
defaults["udpxy"] = ""
|
||||
defaults["version"] = System.DBVersion
|
||||
defaults["xteveAutoUpdate"] = true
|
||||
defaults["temp.path"] = System.Folder.Temp
|
||||
defaults["wizard.completed"] = !freshInstall
|
||||
var defaultTempPath = System.Folder.Temp
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
|
||||
defaultTempPath = System.Folder.Config + "tmp" + string(os.PathSeparator)
|
||||
}
|
||||
defaults["temp.path"] = defaultTempPath
|
||||
|
||||
// Default Werte setzen
|
||||
for key, value := range defaults {
|
||||
@@ -163,7 +183,12 @@ func loadSettings() (settings SettingsStruct, err error) {
|
||||
}
|
||||
|
||||
if len(settings.FFmpegPath) == 0 {
|
||||
settings.FFmpegPath = searchFileInOS("ffmpeg")
|
||||
containerFFmpegPath := "/usr/local/bin/ffmpeg"
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 && checkFile(containerFFmpegPath) == nil {
|
||||
settings.FFmpegPath = containerFFmpegPath
|
||||
} else {
|
||||
settings.FFmpegPath = searchFileInOS("ffmpeg")
|
||||
}
|
||||
}
|
||||
|
||||
if len(settings.VLCPath) == 0 {
|
||||
@@ -201,8 +226,36 @@ func saveSettings(settings SettingsStruct) (err error) {
|
||||
settings.BufferTimeout = 0
|
||||
}
|
||||
|
||||
var userAgent = strings.TrimSpace(settings.UserAgent)
|
||||
if len(userAgent) == 0 || userAgent == System.Name {
|
||||
settings.UserAgent = defaultUserAgent
|
||||
}
|
||||
|
||||
settings.XepgMissingEPGMode = strings.ToLower(strings.TrimSpace(settings.XepgMissingEPGMode))
|
||||
if settings.XepgMissingEPGMode != "relaxed" {
|
||||
settings.XepgMissingEPGMode = "strict"
|
||||
}
|
||||
|
||||
settings.TempPath = strings.TrimRight(settings.TempPath, string(os.PathSeparator)) + string(os.PathSeparator)
|
||||
System.Folder.Temp = settings.TempPath + settings.UUID + string(os.PathSeparator)
|
||||
|
||||
err = checkFolder(System.Folder.Temp)
|
||||
if err != nil {
|
||||
|
||||
fallbackTempPath := System.Folder.Config + "tmp" + string(os.PathSeparator)
|
||||
fallbackTempFolder := fallbackTempPath + settings.UUID + string(os.PathSeparator)
|
||||
fallbackErr := checkFolder(fallbackTempFolder)
|
||||
|
||||
if fallbackErr != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.TempPath = fallbackTempPath
|
||||
System.Folder.Temp = fallbackTempFolder
|
||||
showInfo(fmt.Sprintf("Temporary Folder:Fallback to %s", getPlatformPath(System.Folder.Temp)))
|
||||
|
||||
}
|
||||
|
||||
err = writeByteToFile(System.File.Settings, []byte(mapToJSON(settings)))
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -159,7 +159,6 @@ func searchFileInOS(file string) (path string) {
|
||||
return
|
||||
}
|
||||
|
||||
//
|
||||
func removeChildItems(dir string) error {
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*"))
|
||||
@@ -180,7 +179,7 @@ func removeChildItems(dir string) error {
|
||||
}
|
||||
|
||||
// JSON
|
||||
func mapToJSON(tmpMap interface{}) string {
|
||||
func mapToJSON(tmpMap any) string {
|
||||
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
if err != nil {
|
||||
@@ -190,30 +189,30 @@ func mapToJSON(tmpMap interface{}) string {
|
||||
return string(jsonString)
|
||||
}
|
||||
|
||||
func jsonToMap(content string) map[string]interface{} {
|
||||
func jsonToMap(content string) map[string]any {
|
||||
|
||||
var tmpMap = make(map[string]interface{})
|
||||
var tmpMap = make(map[string]any)
|
||||
json.Unmarshal([]byte(content), &tmpMap)
|
||||
|
||||
return (tmpMap)
|
||||
}
|
||||
|
||||
func jsonToMapInt64(content string) map[int64]interface{} {
|
||||
func jsonToMapInt64(content string) map[int64]any {
|
||||
|
||||
var tmpMap = make(map[int64]interface{})
|
||||
var tmpMap = make(map[int64]any)
|
||||
json.Unmarshal([]byte(content), &tmpMap)
|
||||
|
||||
return (tmpMap)
|
||||
}
|
||||
|
||||
func jsonToInterface(content string) (tmpMap interface{}, err error) {
|
||||
func jsonToInterface(content string) (tmpMap any, err error) {
|
||||
|
||||
err = json.Unmarshal([]byte(content), &tmpMap)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func saveMapToJSONFile(file string, tmpMap interface{}) error {
|
||||
func saveMapToJSONFile(file string, tmpMap any) error {
|
||||
|
||||
var filename = getPlatformFile(file)
|
||||
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
||||
@@ -230,7 +229,7 @@ func saveMapToJSONFile(file string, tmpMap interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadJSONFileToMap(file string) (tmpMap map[string]interface{}, err error) {
|
||||
func loadJSONFileToMap(file string) (tmpMap map[string]any, err error) {
|
||||
|
||||
f, err := os.Open(getPlatformFile(file))
|
||||
defer f.Close()
|
||||
@@ -360,7 +359,7 @@ func randomString(n int) string {
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func parseTemplate(content string, tmpMap map[string]interface{}) (result string) {
|
||||
func parseTemplate(content string, tmpMap map[string]any) (result string) {
|
||||
|
||||
t := template.Must(template.New("template").Parse(content))
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@ func BinaryUpdate() (err error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
err = fmt.Errorf(fmt.Sprintf("Update Server: %s (%s)", http.StatusText(resp.StatusCode), gitInfo))
|
||||
err = fmt.Errorf("Update Server: %s (%s)", http.StatusText(resp.StatusCode), gitInfo)
|
||||
ShowError(err, 6003)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = fmt.Errorf(fmt.Sprintf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), gitInfo))
|
||||
err = fmt.Errorf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), gitInfo)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func BinaryUpdate() (err error) {
|
||||
err = up2date.GetVersion()
|
||||
if err != nil {
|
||||
|
||||
debug = fmt.Sprintf(err.Error())
|
||||
debug = err.Error()
|
||||
showDebug(debug, 1)
|
||||
|
||||
return nil
|
||||
@@ -95,7 +95,7 @@ func BinaryUpdate() (err error) {
|
||||
|
||||
if len(updater.Response.Reason) > 0 {
|
||||
|
||||
err = fmt.Errorf(fmt.Sprintf("Update Server: %s", updater.Response.Reason))
|
||||
err = fmt.Errorf("Update Server: %s", updater.Response.Reason)
|
||||
ShowError(err, 6002)
|
||||
|
||||
return nil
|
||||
@@ -192,7 +192,7 @@ checkVersion:
|
||||
}
|
||||
|
||||
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
|
||||
if oldFilter, ok := settingsMap["filter"].([]interface{}); ok {
|
||||
if oldFilter, ok := settingsMap["filter"].([]any); ok {
|
||||
var newFilterMap = convertToNewFilter(oldFilter)
|
||||
settingsMap["filter"] = newFilterMap
|
||||
|
||||
@@ -252,11 +252,11 @@ checkVersion:
|
||||
return
|
||||
}
|
||||
|
||||
func convertToNewFilter(oldFilter []interface{}) (newFilterMap map[int]interface{}) {
|
||||
func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) {
|
||||
|
||||
newFilterMap = make(map[int]interface{})
|
||||
newFilterMap = make(map[int]any)
|
||||
|
||||
switch reflect.TypeOf(oldFilter).Kind() {
|
||||
switch reflect.TypeFor[[]any]().Kind() {
|
||||
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(oldFilter)
|
||||
@@ -285,7 +285,7 @@ func setValueForUUID() (err error) {
|
||||
|
||||
for _, c := range xepg {
|
||||
|
||||
var xepgChannel = c.(map[string]interface{})
|
||||
var xepgChannel = c.(map[string]any)
|
||||
|
||||
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {
|
||||
|
||||
|
||||
15
src/user_agent.go
Normal file
15
src/user_agent.go
Normal 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
|
||||
}
|
||||
86
src/webUI.go
86
src/webUI.go
File diff suppressed because one or more lines are too long
@@ -15,6 +15,14 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// StartWebserver : Startet den Webserver
|
||||
func StartWebserver() (err error) {
|
||||
|
||||
@@ -336,7 +344,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
*/
|
||||
|
||||
conn, err := websocket.Upgrade(w, r, w.Header(), 1024, 1024)
|
||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
|
||||
@@ -558,7 +566,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
fmt.Println("+ + + + + + + + + + +", request.Cmd)
|
||||
|
||||
var requestMap = make(map[string]interface{}) // Debug
|
||||
var requestMap = make(map[string]any) // Debug
|
||||
_ = requestMap
|
||||
if System.Dev == true {
|
||||
fmt.Println(mapToJSON(requestMap))
|
||||
@@ -591,7 +599,7 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
// Web : Web Server /web/
|
||||
func Web(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var lang = make(map[string]interface{})
|
||||
var lang = make(map[string]any)
|
||||
var err error
|
||||
|
||||
var requestFile = strings.Replace(r.URL.Path, "/web", "html", -1)
|
||||
@@ -629,7 +637,7 @@ func Web(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if System.ScanInProgress == 0 {
|
||||
|
||||
if len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 {
|
||||
if Settings.WizardCompleted == false && len(Settings.Files.M3U) == 0 && len(Settings.Files.HDHR) == 0 {
|
||||
System.ConfigurationWizard = true
|
||||
}
|
||||
|
||||
@@ -1028,7 +1036,7 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
defaults.ClientInfo.XEPGCount = Data.XEPG.XEPGCount
|
||||
|
||||
var XEPG = make(map[string]interface{})
|
||||
var XEPG = make(map[string]any)
|
||||
|
||||
if len(Data.Streams.Active) > 0 {
|
||||
|
||||
@@ -1037,8 +1045,8 @@ func setDefaultResponseData(response ResponseStruct, data bool) (defaults Respon
|
||||
|
||||
} else {
|
||||
|
||||
XEPG["epgMapping"] = make(map[string]interface{})
|
||||
XEPG["xmltvMap"] = make(map[string]interface{})
|
||||
XEPG["epgMapping"] = make(map[string]any)
|
||||
XEPG["xmltvMap"] = make(map[string]any)
|
||||
|
||||
}
|
||||
|
||||
|
||||
257
src/xepg.go
257
src/xepg.go
@@ -9,6 +9,7 @@ import (
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
@@ -68,6 +69,7 @@ func buildXEPG(background bool) {
|
||||
cleanupXEPG()
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg rebuild")
|
||||
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
@@ -84,6 +86,7 @@ func buildXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg image cache refresh")
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
@@ -113,6 +116,7 @@ func buildXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg rebuild")
|
||||
|
||||
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 {
|
||||
|
||||
@@ -127,6 +131,7 @@ func buildXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg image cache refresh")
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
@@ -179,6 +184,7 @@ func updateXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg update")
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
System.ScanInProgress = 0
|
||||
@@ -207,9 +213,9 @@ func updateXEPG(background bool) {
|
||||
func createXEPGMapping() {
|
||||
|
||||
Data.XMLTV.Files = getLocalProviderFiles("xmltv")
|
||||
Data.XMLTV.Mapping = make(map[string]interface{})
|
||||
Data.XMLTV.Mapping = make(map[string]any)
|
||||
|
||||
var tmpMap = make(map[string]interface{})
|
||||
var tmpMap = make(map[string]any)
|
||||
|
||||
var friendlyDisplayName = func(channel Channel) (displayName string) {
|
||||
var dn = channel.DisplayName
|
||||
@@ -250,10 +256,10 @@ func createXEPGMapping() {
|
||||
if err == nil {
|
||||
|
||||
// Daten aus der XML Datei in eine temporäre Map schreiben
|
||||
var xmltvMap = make(map[string]interface{})
|
||||
var xmltvMap = make(map[string]any)
|
||||
|
||||
for _, c := range xmltv.Channel {
|
||||
var channel = make(map[string]interface{})
|
||||
var channel = make(map[string]any)
|
||||
|
||||
channel["id"] = c.ID
|
||||
channel["display-name"] = friendlyDisplayName(*c)
|
||||
@@ -271,7 +277,7 @@ func createXEPGMapping() {
|
||||
}
|
||||
|
||||
Data.XMLTV.Mapping = tmpMap
|
||||
tmpMap = make(map[string]interface{})
|
||||
tmpMap = make(map[string]any)
|
||||
|
||||
} else {
|
||||
|
||||
@@ -282,7 +288,7 @@ func createXEPGMapping() {
|
||||
}
|
||||
|
||||
// Auswahl für den Dummy erstellen
|
||||
var dummy = make(map[string]interface{})
|
||||
var dummy = make(map[string]any)
|
||||
var times = []string{"30", "60", "90", "120", "180", "240", "360"}
|
||||
|
||||
for _, i := range times {
|
||||
@@ -306,7 +312,7 @@ func createXEPGDatabase() (err error) {
|
||||
|
||||
var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit)
|
||||
Data.Cache.Streams.Active = make([]string, 0, System.UnfilteredChannelLimit)
|
||||
Data.XEPG.Channels = make(map[string]interface{}, System.UnfilteredChannelLimit)
|
||||
Data.XEPG.Channels = make(map[string]any, System.UnfilteredChannelLimit)
|
||||
|
||||
Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG)
|
||||
if err != nil {
|
||||
@@ -521,10 +527,132 @@ func createXEPGDatabase() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func normalizeXEPGMatchValue(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case unicode.IsLetter(r):
|
||||
return unicode.ToLower(r)
|
||||
case unicode.IsDigit(r):
|
||||
return r
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}, value)
|
||||
}
|
||||
|
||||
func appendXEPGIssueSample(samples map[string]struct{}, value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) == 0 {
|
||||
return
|
||||
}
|
||||
samples[value] = struct{}{}
|
||||
}
|
||||
|
||||
func xepgIssueSampleText(samples map[string]struct{}, max int) string {
|
||||
if len(samples) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
list := make([]string, 0, len(samples))
|
||||
for name := range samples {
|
||||
list = append(list, name)
|
||||
}
|
||||
sort.Strings(list)
|
||||
|
||||
if len(list) > max {
|
||||
return strings.Join(list[:max], ", ") + ", ..."
|
||||
}
|
||||
|
||||
return strings.Join(list, ", ")
|
||||
}
|
||||
|
||||
func findXEPGReplacementChannel(xmltvChannels map[string]any, xepgChannel XEPGChannelStruct) (channelID string, channel map[string]any, ok bool) {
|
||||
var candidateValues = map[string]struct{}{}
|
||||
|
||||
addCandidate := func(value string) {
|
||||
normalized := normalizeXEPGMatchValue(value)
|
||||
if len(normalized) == 0 {
|
||||
return
|
||||
}
|
||||
candidateValues[normalized] = struct{}{}
|
||||
}
|
||||
|
||||
addCandidate(xepgChannel.XName)
|
||||
addCandidate(xepgChannel.Name)
|
||||
addCandidate(xepgChannel.TvgName)
|
||||
addCandidate(xepgChannel.TvgID)
|
||||
|
||||
if len(candidateValues) == 0 {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
if _, exists := candidateValues[normalizeXEPGMatchValue(xepgChannel.TvgID)]; exists {
|
||||
if direct, found := xmltvChannels[xepgChannel.TvgID].(map[string]any); found {
|
||||
return xepgChannel.TvgID, direct, true
|
||||
}
|
||||
}
|
||||
|
||||
var matches []struct {
|
||||
id string
|
||||
channel map[string]any
|
||||
}
|
||||
|
||||
for id, data := range xmltvChannels {
|
||||
xmltvChannel, castOK := data.(map[string]any)
|
||||
if castOK == false {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, match := candidateValues[normalizeXEPGMatchValue(id)]; match {
|
||||
matches = append(matches, struct {
|
||||
id string
|
||||
channel map[string]any
|
||||
}{id: id, channel: xmltvChannel})
|
||||
continue
|
||||
}
|
||||
|
||||
displayName, hasDisplayName := xmltvChannel["display-name"].(string)
|
||||
if hasDisplayName == false {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, match := candidateValues[normalizeXEPGMatchValue(displayName)]; match {
|
||||
matches = append(matches, struct {
|
||||
id string
|
||||
channel map[string]any
|
||||
}{id: id, channel: xmltvChannel})
|
||||
}
|
||||
}
|
||||
|
||||
if len(matches) != 1 {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
return matches[0].id, matches[0].channel, true
|
||||
}
|
||||
|
||||
// Kanäle automatisch zuordnen und das Mapping überprüfen
|
||||
func mapping() (err error) {
|
||||
showInfo("XEPG:" + "Map channels")
|
||||
|
||||
strictMissingEPGMode := Settings.XepgMissingEPGMode != "relaxed"
|
||||
|
||||
missingEPGCount := 0
|
||||
missingXMLTVCount := 0
|
||||
autoRemapCount := 0
|
||||
relaxedKeepCount := 0
|
||||
relaxedDummyCount := 0
|
||||
missingEPGSamples := map[string]struct{}{}
|
||||
missingXMLTVSamples := map[string]struct{}{}
|
||||
autoRemapSamples := map[string]struct{}{}
|
||||
relaxedKeepSamples := map[string]struct{}{}
|
||||
relaxedDummySamples := map[string]struct{}{}
|
||||
|
||||
for xepg, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
@@ -537,7 +665,7 @@ func mapping() (err error) {
|
||||
if xepgChannel.XActive == false {
|
||||
|
||||
// Werte kann "-" sein, deswegen len < 1
|
||||
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XmltvFile) < 1 {
|
||||
if len(xepgChannel.XmltvFile) < 1 && len(xepgChannel.XMapping) < 1 {
|
||||
|
||||
var tvgID = xepgChannel.TvgID
|
||||
|
||||
@@ -549,16 +677,16 @@ func mapping() (err error) {
|
||||
|
||||
for file, xmltvChannels := range Data.XMLTV.Mapping {
|
||||
|
||||
if channel, ok := xmltvChannels.(map[string]interface{})[tvgID]; ok {
|
||||
if channel, ok := xmltvChannels.(map[string]any)[tvgID]; ok {
|
||||
|
||||
if channelID, ok := channel.(map[string]interface{})["id"].(string); ok {
|
||||
if channelID, ok := channel.(map[string]any)["id"].(string); ok {
|
||||
|
||||
xepgChannel.XmltvFile = file
|
||||
xepgChannel.XMapping = channelID
|
||||
xepgChannel.XActive = true
|
||||
|
||||
// Falls in der XMLTV Datei ein Logo existiert, wird dieses verwendet. Falls nicht, dann das Logo aus der M3U Datei
|
||||
if icon, ok := channel.(map[string]interface{})["icon"].(string); ok {
|
||||
if icon, ok := channel.(map[string]any)["icon"].(string); ok {
|
||||
if len(icon) > 0 {
|
||||
xepgChannel.TvgLogo = icon
|
||||
}
|
||||
@@ -585,9 +713,9 @@ func mapping() (err error) {
|
||||
|
||||
if file != "xTeVe Dummy" {
|
||||
|
||||
if value, ok := Data.XMLTV.Mapping[file].(map[string]interface{}); ok {
|
||||
if value, ok := Data.XMLTV.Mapping[file].(map[string]any); ok {
|
||||
|
||||
if channel, ok := value[mapping].(map[string]interface{}); ok {
|
||||
if channel, ok := value[mapping].(map[string]any); ok {
|
||||
|
||||
// Kanallogo aktualisieren
|
||||
if logo, ok := channel["icon"].(string); ok {
|
||||
@@ -599,20 +727,68 @@ func mapping() (err error) {
|
||||
}
|
||||
|
||||
} else {
|
||||
if channelID, replacementChannel, remapOK := findXEPGReplacementChannel(value, xepgChannel); remapOK {
|
||||
xepgChannel.XMapping = channelID
|
||||
|
||||
ShowError(fmt.Errorf(fmt.Sprintf("Missing EPG data: %s", xepgChannel.Name)), 0)
|
||||
showWarning(2302)
|
||||
xepgChannel.XActive = false
|
||||
if logo, ok := replacementChannel["icon"].(string); ok {
|
||||
if xepgChannel.XUpdateChannelIcon == true && len(logo) > 0 {
|
||||
xepgChannel.TvgLogo = logo
|
||||
}
|
||||
}
|
||||
|
||||
autoRemapCount++
|
||||
name := strings.TrimSpace(xepgChannel.Name)
|
||||
if len(name) == 0 {
|
||||
name = strings.TrimSpace(xepgChannel.XName)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
name = xepg
|
||||
}
|
||||
appendXEPGIssueSample(autoRemapSamples, name)
|
||||
} else {
|
||||
name := strings.TrimSpace(xepgChannel.Name)
|
||||
if len(name) == 0 {
|
||||
name = strings.TrimSpace(xepgChannel.XName)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
name = xepg
|
||||
}
|
||||
|
||||
if strictMissingEPGMode == true {
|
||||
missingEPGCount++
|
||||
appendXEPGIssueSample(missingEPGSamples, name)
|
||||
xepgChannel.XActive = false
|
||||
} else {
|
||||
relaxedKeepCount++
|
||||
appendXEPGIssueSample(relaxedKeepSamples, name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var fileID = strings.TrimSuffix(getFilenameFromPath(file), path.Ext(getFilenameFromPath(file)))
|
||||
providerName := getProviderParameter(fileID, "xmltv", "name")
|
||||
if len(strings.TrimSpace(providerName)) == 0 {
|
||||
providerName = file
|
||||
}
|
||||
|
||||
ShowError(fmt.Errorf("Missing XMLTV file: %s", getProviderParameter(fileID, "xmltv", "name")), 0)
|
||||
showWarning(2301)
|
||||
xepgChannel.XActive = false
|
||||
if strictMissingEPGMode == true {
|
||||
missingXMLTVCount++
|
||||
appendXEPGIssueSample(missingXMLTVSamples, providerName)
|
||||
xepgChannel.XActive = false
|
||||
} else {
|
||||
relaxedDummyCount++
|
||||
name := strings.TrimSpace(xepgChannel.Name)
|
||||
if len(name) == 0 {
|
||||
name = strings.TrimSpace(xepgChannel.XName)
|
||||
}
|
||||
if len(name) == 0 {
|
||||
name = xepg
|
||||
}
|
||||
appendXEPGIssueSample(relaxedDummySamples, name)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -634,6 +810,28 @@ func mapping() (err error) {
|
||||
|
||||
}
|
||||
|
||||
if autoRemapCount > 0 {
|
||||
showInfo(fmt.Sprintf("XEPG:%d channel mappings were auto-remapped (examples: %s)", autoRemapCount, xepgIssueSampleText(autoRemapSamples, 8)))
|
||||
}
|
||||
|
||||
if missingEPGCount > 0 {
|
||||
showWarning(2302)
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels have missing EPG mappings and were deactivated (examples: %s)", missingEPGCount, xepgIssueSampleText(missingEPGSamples, 8)))
|
||||
}
|
||||
|
||||
if missingXMLTVCount > 0 {
|
||||
showWarning(2301)
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels reference missing XMLTV files and were deactivated (sources: %s)", missingXMLTVCount, xepgIssueSampleText(missingXMLTVSamples, 5)))
|
||||
}
|
||||
|
||||
if relaxedKeepCount > 0 {
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels kept active in relaxed mode despite missing EPG mappings (examples: %s)", relaxedKeepCount, xepgIssueSampleText(relaxedKeepSamples, 8)))
|
||||
}
|
||||
|
||||
if relaxedDummyCount > 0 {
|
||||
showInfo(fmt.Sprintf("XEPG:%d channels will use xTeVe Dummy guide in relaxed mode because XMLTV sources were unavailable (examples: %s)", relaxedDummyCount, xepgIssueSampleText(relaxedDummySamples, 8)))
|
||||
}
|
||||
|
||||
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -667,7 +865,7 @@ func createXMLTVFile() (err error) {
|
||||
}
|
||||
|
||||
if len(Data.XMLTV.Files) == 0 && len(Data.Streams.Active) == 0 {
|
||||
Data.XEPG.Channels = make(map[string]interface{})
|
||||
Data.XEPG.Channels = make(map[string]any)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -735,6 +933,13 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
|
||||
var xmltvFile = System.Folder.Data + xepgChannel.XmltvFile
|
||||
var channelID = xepgChannel.XMapping
|
||||
relaxedMissingEPGMode := Settings.XepgMissingEPGMode == "relaxed"
|
||||
fallbackToDummy := func() {
|
||||
dummyChannel := xepgChannel
|
||||
dummyChannel.XmltvFile = "xTeVe Dummy"
|
||||
dummyChannel.XMapping = "240_Minutes"
|
||||
xepgXML = createDummyProgram(dummyChannel)
|
||||
}
|
||||
|
||||
var xmltv XMLTV
|
||||
|
||||
@@ -744,6 +949,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
|
||||
err = getLocalXMLTV(xmltvFile, &xmltv)
|
||||
if err != nil {
|
||||
if relaxedMissingEPGMode == true {
|
||||
fallbackToDummy()
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -817,6 +1026,10 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
|
||||
}
|
||||
|
||||
if len(xepgXML.Program) == 0 && relaxedMissingEPGMode == true && xmltvFile != System.Folder.Data+"xTeVe Dummy" {
|
||||
fallbackToDummy()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -830,7 +1043,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
var currentDay = currentTime.Format("20060102")
|
||||
var startTime, _ = time.Parse("20060102150405", currentDay+"000000")
|
||||
|
||||
showInfo("Create Dummy Guide:" + "Time offset" + offset + " - " + xepgChannel.XName)
|
||||
showDebug("Create Dummy Guide:"+"Time offset"+offset+" - "+xepgChannel.XName, 2)
|
||||
|
||||
var dl = strings.Split(xepgChannel.XMapping, "_")
|
||||
dummyLength, err := strconv.Atoi(dl[0])
|
||||
@@ -839,7 +1052,7 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
return
|
||||
}
|
||||
|
||||
for d := 0; d < 4; d++ {
|
||||
for d := range 4 {
|
||||
|
||||
var epgStartTime = startTime.Add(time.Hour * time.Duration(d*24))
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ function login() {
|
||||
if (value.length == 0) {
|
||||
inputs[i].style.borderColor = "red"
|
||||
err = true
|
||||
} else {
|
||||
inputs[i].style.borderColor = ""
|
||||
}
|
||||
|
||||
data[key] = value
|
||||
@@ -30,7 +32,6 @@ function login() {
|
||||
if (data.hasOwnProperty("confirm")) {
|
||||
|
||||
if (data["confirm"] != data["password"]) {
|
||||
alert("sdafsd")
|
||||
document.getElementById('password').style.borderColor = "red"
|
||||
document.getElementById('confirm').style.borderColor = "red"
|
||||
|
||||
|
||||
114
ts/base_ts.ts
114
ts/base_ts.ts
@@ -5,6 +5,8 @@ var SEARCH_MAPPING = new Object()
|
||||
var UNDO = new Object()
|
||||
var SERVER_CONNECTION = false
|
||||
var WS_AVAILABLE = false
|
||||
var ACTIVE_MENU_ID:string = ""
|
||||
var LAST_BULK_CHECKBOX:HTMLInputElement = null
|
||||
|
||||
|
||||
// Menü
|
||||
@@ -21,7 +23,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
|
||||
|
||||
// Kategorien für die Einstellungen
|
||||
var settingsCategory = new Array()
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"))
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"))
|
||||
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))
|
||||
@@ -51,7 +53,44 @@ function showElement(elmID, type) {
|
||||
case false: cssClass = "none"; break;
|
||||
}
|
||||
|
||||
document.getElementById(elmID).className = cssClass;
|
||||
var element = document.getElementById(elmID)
|
||||
if (element == null) {
|
||||
return
|
||||
}
|
||||
|
||||
element.className = cssClass;
|
||||
}
|
||||
|
||||
function setConnectionState(state:string, text:string = "") {
|
||||
|
||||
var label:string = text
|
||||
if (label == undefined || label.length == 0) {
|
||||
switch (state) {
|
||||
case "online":
|
||||
label = "Connected"
|
||||
break
|
||||
|
||||
case "busy":
|
||||
label = "Syncing"
|
||||
break
|
||||
|
||||
case "offline":
|
||||
label = "Offline"
|
||||
break
|
||||
|
||||
default:
|
||||
label = "Connecting"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var indicator = document.getElementById("connection-indicator")
|
||||
if (indicator == null) {
|
||||
return
|
||||
}
|
||||
|
||||
indicator.className = "status-" + state
|
||||
indicator.innerText = label
|
||||
}
|
||||
|
||||
function changeButtonAction(element, buttonID, attribute) {
|
||||
@@ -144,6 +183,59 @@ function getAllSelectedChannels():string[] {
|
||||
return channels
|
||||
}
|
||||
|
||||
function scheduleChannelRangeSelection(checkbox:HTMLInputElement, event:MouseEvent) {
|
||||
|
||||
var shiftPressed = false
|
||||
if (event != undefined && event.shiftKey == true) {
|
||||
shiftPressed = true
|
||||
}
|
||||
|
||||
// Run after the native checkbox toggle so we copy the final checked state.
|
||||
setTimeout(function() {
|
||||
selectChannelRange(checkbox, shiftPressed)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function selectChannelRange(checkbox:HTMLInputElement, shiftPressed:boolean) {
|
||||
|
||||
if (BULK_EDIT == false || checkbox == undefined || checkbox == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var table = document.getElementById("content_table")
|
||||
if (table == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var trs = table.getElementsByTagName("TR")
|
||||
var visibleCheckboxes:HTMLInputElement[] = new Array()
|
||||
|
||||
for (var i = 1; i < trs.length; i++) {
|
||||
if ((trs[i] as HTMLElement).style.display != "none") {
|
||||
var bulkCheckbox = (trs[i] as HTMLTableRowElement).querySelector("input.bulk") as HTMLInputElement
|
||||
if (bulkCheckbox != null) {
|
||||
visibleCheckboxes.push(bulkCheckbox)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentIndex = visibleCheckboxes.indexOf(checkbox)
|
||||
var previousIndex = -1
|
||||
if (LAST_BULK_CHECKBOX != null) {
|
||||
previousIndex = visibleCheckboxes.indexOf(LAST_BULK_CHECKBOX)
|
||||
}
|
||||
|
||||
if (shiftPressed == true && previousIndex > -1 && currentIndex > -1) {
|
||||
var start = Math.min(previousIndex, currentIndex)
|
||||
var end = Math.max(previousIndex, currentIndex)
|
||||
for (var i = start; i <= end; i++) {
|
||||
visibleCheckboxes[i].checked = checkbox.checked
|
||||
}
|
||||
}
|
||||
|
||||
LAST_BULK_CHECKBOX = checkbox
|
||||
}
|
||||
|
||||
function selectAllChannels() {
|
||||
|
||||
var bulk:Boolean = false
|
||||
@@ -173,6 +265,7 @@ function selectAllChannels() {
|
||||
|
||||
}
|
||||
|
||||
LAST_BULK_CHECKBOX = null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -197,6 +290,7 @@ function bulkEdit() {
|
||||
(rows[i] as HTMLInputElement).checked = false
|
||||
}
|
||||
|
||||
LAST_BULK_CHECKBOX = null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -379,17 +473,19 @@ function searchInMapping() {
|
||||
|
||||
function calculateWrapperHeight() {
|
||||
|
||||
if (document.getElementById("box-wrapper")){
|
||||
var elm = document.getElementById("box-wrapper");
|
||||
var content = document.getElementById("content");
|
||||
|
||||
var elm = document.getElementById("box-wrapper");
|
||||
if (elm != null && content != null){
|
||||
|
||||
var divs = new Array("myStreamsBox", "clientInfo", "content");
|
||||
var elementsHeight = 0 - elm.offsetHeight;
|
||||
for (var i = 0; i < divs.length; i++) {
|
||||
elementsHeight = elementsHeight + document.getElementById(divs[i]).offsetHeight;
|
||||
var contentTop = content.getBoundingClientRect().top
|
||||
var freeSpace = window.innerHeight - contentTop - 26
|
||||
|
||||
if (freeSpace < 180) {
|
||||
freeSpace = 180
|
||||
}
|
||||
|
||||
elm.style.height = window.innerHeight - elementsHeight + "px";
|
||||
elm.style.height = freeSpace + "px";
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,18 @@ function showLogs(bottom:boolean) {
|
||||
|
||||
var logs = SERVER["log"]["log"]
|
||||
var div = document.getElementById("content_log")
|
||||
var wrapper = document.getElementById("box-wrapper") as HTMLElement
|
||||
var shouldStickToBottom:boolean = bottom
|
||||
|
||||
div.innerHTML = ""
|
||||
|
||||
if (wrapper != null && shouldStickToBottom == false) {
|
||||
var distanceToBottom:number = wrapper.scrollHeight - wrapper.scrollTop - wrapper.clientHeight
|
||||
if (distanceToBottom < 80) {
|
||||
shouldStickToBottom = true
|
||||
}
|
||||
}
|
||||
|
||||
var keys = getObjKeys(logs)
|
||||
|
||||
keys.forEach(logID => {
|
||||
@@ -44,9 +53,8 @@ function showLogs(bottom:boolean) {
|
||||
|
||||
setTimeout(function(){
|
||||
|
||||
if (bottom == true) {
|
||||
if (shouldStickToBottom == true && wrapper != null) {
|
||||
|
||||
var wrapper = document.getElementById("box-wrapper");
|
||||
wrapper.scrollTop = wrapper.scrollHeight;
|
||||
|
||||
}
|
||||
|
||||
321
ts/menu_ts.ts
321
ts/menu_ts.ts
@@ -37,6 +37,7 @@ class MainMenuItem extends MainMenu {
|
||||
var item = document.createElement("LI")
|
||||
item.setAttribute("onclick", "javascript: openThisMenu(this)")
|
||||
item.setAttribute("id", this.id)
|
||||
item.setAttribute("data-menu", this.menuKey)
|
||||
var img = this.createIMG(this.imgSrc)
|
||||
var value = this.createValue(this.value)
|
||||
|
||||
@@ -64,7 +65,7 @@ class MainMenuItem extends MainMenu {
|
||||
break
|
||||
|
||||
case "mapping":
|
||||
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}"]
|
||||
this.tableHeader = ["BULK", "{{.mapping.table.chNo}}", "{{.mapping.table.logo}}", "{{.mapping.table.channelName}}", "{{.mapping.table.playlist}}", "{{.mapping.table.groupTitle}}", "{{.mapping.table.xmltvFile}}", "{{.mapping.table.xmltvID}}", "{{.mapping.table.edit}}"]
|
||||
break
|
||||
|
||||
}
|
||||
@@ -424,11 +425,7 @@ class Content {
|
||||
cell.child = true
|
||||
cell.childType = "IMG"
|
||||
cell.imageURL = data[key]["tvg-logo"]
|
||||
var td = cell.createCell()
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
td.id = key
|
||||
|
||||
tr.appendChild(td)
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// Kanalname
|
||||
var cell:Cell = new Cell()
|
||||
@@ -436,10 +433,7 @@ class Content {
|
||||
cell.childType = "P"
|
||||
cell.className = data[key]["x-category"]
|
||||
cell.value = data[key]["x-name"]
|
||||
var td = cell.createCell()
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
td.id = key
|
||||
tr.appendChild(td)
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
|
||||
// Playlist
|
||||
@@ -448,10 +442,7 @@ class Content {
|
||||
cell.childType = "P"
|
||||
//cell.value = data[key]["_file.m3u.name"]
|
||||
cell.value = getValueFromProviderFile(data[key]["_file.m3u.id"], "m3u", "name")
|
||||
var td = cell.createCell()
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
td.id = key
|
||||
tr.appendChild(td)
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
|
||||
// Gruppe (group-title)
|
||||
@@ -459,10 +450,7 @@ class Content {
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
cell.value = data[key]["x-group-title"]
|
||||
var td = cell.createCell()
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
td.id = key
|
||||
tr.appendChild(td)
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// XMLTV Datei
|
||||
var cell:Cell = new Cell()
|
||||
@@ -475,10 +463,7 @@ class Content {
|
||||
cell.value = data[key]["x-xmltv-file"]
|
||||
}
|
||||
|
||||
var td = cell.createCell()
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
td.id = key
|
||||
tr.appendChild(td)
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
// XMLTV Kanal
|
||||
var cell:Cell = new Cell()
|
||||
@@ -490,11 +475,18 @@ class Content {
|
||||
value = data[key]["x-mapping"].substring(0, 20) + "..."
|
||||
}
|
||||
cell.value = value
|
||||
var td = cell.createCell()
|
||||
td.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
td.id = key
|
||||
tr.appendChild(cell.createCell())
|
||||
|
||||
tr.appendChild(td)
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "EDIT"
|
||||
cell.value = "{{.button.edit}}"
|
||||
var editTd = cell.createCell()
|
||||
var editButton = (editTd.firstChild as HTMLInputElement)
|
||||
editButton.setAttribute('onclick', 'javascript: openPopUp("mapping", this)')
|
||||
editButton.setAttribute("id", key)
|
||||
editButton.setAttribute("aria-label", "Edit " + data[key]["x-name"])
|
||||
tr.appendChild(editTd)
|
||||
|
||||
rows.push(tr)
|
||||
});
|
||||
@@ -561,6 +553,9 @@ class Cell {
|
||||
(element as HTMLInputElement).checked = this.value;
|
||||
(element as HTMLInputElement).type = "checkbox";
|
||||
(element as HTMLInputElement).className = "bulk hideBulk";
|
||||
(element as HTMLInputElement).addEventListener("click", function(event) {
|
||||
scheduleChannelRangeSelection((element as HTMLInputElement), (event as MouseEvent))
|
||||
})
|
||||
break
|
||||
|
||||
case "BULK_HEAD":
|
||||
@@ -578,6 +573,14 @@ class Cell {
|
||||
element.setAttribute("onerror", "javascript: this.onerror=null;this.src=''" )
|
||||
//onerror="this.onerror=null;this.src='missing.gif';"
|
||||
}
|
||||
break
|
||||
|
||||
case "EDIT":
|
||||
element = document.createElement("INPUT");
|
||||
(element as HTMLInputElement).type = "button";
|
||||
(element as HTMLInputElement).value = this.value;
|
||||
(element as HTMLInputElement).className = "mapping-edit-button";
|
||||
break
|
||||
}
|
||||
|
||||
td.appendChild(element)
|
||||
@@ -683,7 +686,7 @@ class ShowContent extends Content {
|
||||
input.setAttribute("id", "searchMapping")
|
||||
input.setAttribute("placeholder", "{{.button.search}}")
|
||||
input.className = "search"
|
||||
input.setAttribute("onchange", 'javascript: searchInMapping()')
|
||||
input.setAttribute("oninput", 'javascript: searchInMapping()')
|
||||
interaction.appendChild(input)
|
||||
break;
|
||||
|
||||
@@ -824,16 +827,227 @@ class ShowContent extends Content {
|
||||
|
||||
}
|
||||
|
||||
var SHELL_LAYOUT_READY:boolean = false
|
||||
|
||||
function setLayoutMenuState(open:boolean) {
|
||||
if (document.body == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (open == true) {
|
||||
document.body.classList.add("menu-open")
|
||||
} else {
|
||||
document.body.classList.remove("menu-open")
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLayoutMenu() {
|
||||
if (document.body == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var isOpen:boolean = document.body.classList.contains("menu-open")
|
||||
setLayoutMenuState(!isOpen)
|
||||
}
|
||||
|
||||
function closeLayoutMenuIfMobile() {
|
||||
if (window.innerWidth <= 900) {
|
||||
setLayoutMenuState(false)
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveMenu(menuID:string) {
|
||||
|
||||
ACTIVE_MENU_ID = menuID.toString()
|
||||
var menu = document.getElementById("main-menu")
|
||||
if (menu == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var items = menu.getElementsByTagName("LI")
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
items[i].classList.remove("menu-active")
|
||||
}
|
||||
|
||||
var activeItem = document.getElementById(ACTIVE_MENU_ID)
|
||||
var activeMenuKey:string = ""
|
||||
if (activeItem != null) {
|
||||
activeItem.classList.add("menu-active")
|
||||
var menuKeyValue = activeItem.getAttribute("data-menu")
|
||||
if (menuKeyValue != null) {
|
||||
activeMenuKey = menuKeyValue
|
||||
}
|
||||
}
|
||||
|
||||
if (document.body != null) {
|
||||
if (activeMenuKey.length > 0) {
|
||||
document.body.setAttribute("data-active-menu", activeMenuKey)
|
||||
} else {
|
||||
document.body.removeAttribute("data-active-menu")
|
||||
}
|
||||
|
||||
if (activeMenuKey == "log") {
|
||||
document.body.classList.add("menu-log-focus")
|
||||
} else {
|
||||
document.body.classList.remove("menu-log-focus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatusCards() {
|
||||
var wrapper = document.getElementById("status-cards")
|
||||
if (wrapper == null || SERVER.hasOwnProperty("clientInfo") == false) {
|
||||
return
|
||||
}
|
||||
|
||||
var info = SERVER["clientInfo"]
|
||||
var errors:number = parseInt(info["errors"], 10)
|
||||
var warnings:number = parseInt(info["warnings"], 10)
|
||||
var cards:any[] = [
|
||||
{label: "Streams", value: info["streams"], tone: "ok"},
|
||||
{label: "EPG Source", value: info["epgSource"], tone: "neutral"},
|
||||
{label: "XEPG Channels", value: info["xepg"], tone: "ok"},
|
||||
{label: "Errors", value: info["errors"], tone: errors > 0 ? "error" : "ok"},
|
||||
{label: "Warnings", value: info["warnings"], tone: warnings > 0 ? "warn" : "ok"},
|
||||
{label: "DVR", value: info["DVR"], tone: "neutral"},
|
||||
]
|
||||
|
||||
wrapper.innerHTML = ""
|
||||
cards.forEach(card => {
|
||||
|
||||
var box = document.createElement("DIV")
|
||||
box.className = "status-card status-card-" + card.tone
|
||||
|
||||
var label = document.createElement("P")
|
||||
label.className = "status-card-label"
|
||||
label.innerText = card.label
|
||||
|
||||
var value = document.createElement("P")
|
||||
value.className = "status-card-value"
|
||||
if (card.value == undefined || card.value == "") {
|
||||
value.innerText = "-"
|
||||
} else {
|
||||
value.innerText = card.value
|
||||
}
|
||||
|
||||
box.appendChild(label)
|
||||
box.appendChild(value)
|
||||
wrapper.appendChild(box)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function initShellLayout() {
|
||||
|
||||
if (SHELL_LAYOUT_READY == true) {
|
||||
return
|
||||
}
|
||||
|
||||
var toggle = document.getElementById("menu-toggle")
|
||||
if (toggle != null) {
|
||||
toggle.onclick = function() {
|
||||
toggleLayoutMenu()
|
||||
}
|
||||
}
|
||||
|
||||
var overlay = document.getElementById("layout-overlay")
|
||||
if (overlay != null) {
|
||||
overlay.onclick = function() {
|
||||
setLayoutMenuState(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", function(event) {
|
||||
|
||||
if (event.key == "Escape") {
|
||||
setLayoutMenuState(false)
|
||||
showElement("popup", false)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key == "/") {
|
||||
var target = event.target as HTMLElement
|
||||
var onInput = target.tagName == "INPUT" || target.tagName == "TEXTAREA" || target.tagName == "SELECT"
|
||||
|
||||
if (onInput == true) {
|
||||
return
|
||||
}
|
||||
|
||||
var search = document.getElementById("searchMapping")
|
||||
if (search != null) {
|
||||
event.preventDefault()
|
||||
(search as HTMLInputElement).focus()
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
setConnectionState("idle")
|
||||
SHELL_LAYOUT_READY = true
|
||||
}
|
||||
|
||||
function shouldPollLogs():boolean {
|
||||
if (document.hidden == true) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById("content_log") == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU_ID.length < 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
var activeItem = document.getElementById(ACTIVE_MENU_ID)
|
||||
if (activeItem == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return activeItem.getAttribute("data-menu") == "log"
|
||||
}
|
||||
|
||||
function PageReady() {
|
||||
|
||||
initShellLayout()
|
||||
|
||||
var server:Server = new Server("getServerConfig")
|
||||
server.request(new Object())
|
||||
|
||||
var bootstrapAttempts:number = 0
|
||||
var maxBootstrapAttempts:number = 5
|
||||
var bootstrapTimer:number = window.setInterval(function() {
|
||||
|
||||
if (SERVER.hasOwnProperty("clientInfo") == true) {
|
||||
window.clearInterval(bootstrapTimer)
|
||||
return
|
||||
}
|
||||
|
||||
if (SERVER_CONNECTION == true) {
|
||||
return
|
||||
}
|
||||
|
||||
bootstrapAttempts++
|
||||
var retryServer:Server = new Server("getServerConfig")
|
||||
retryServer.request(new Object())
|
||||
|
||||
if (bootstrapAttempts >= maxBootstrapAttempts) {
|
||||
window.clearInterval(bootstrapTimer)
|
||||
}
|
||||
|
||||
}, 3000)
|
||||
|
||||
window.addEventListener("resize", function(){
|
||||
if (window.innerWidth > 900) {
|
||||
setLayoutMenuState(false)
|
||||
}
|
||||
calculateWrapperHeight();
|
||||
}, true);
|
||||
|
||||
setInterval(function(){
|
||||
if (shouldPollLogs() == false) {
|
||||
return
|
||||
}
|
||||
updateLog()
|
||||
}, 10000);
|
||||
|
||||
@@ -841,6 +1055,35 @@ function PageReady() {
|
||||
return
|
||||
}
|
||||
|
||||
function isClientInfoHttpURL(value:string):boolean {
|
||||
return /^https?:\/\//i.test(value)
|
||||
}
|
||||
|
||||
function setClientInfoValue(key:string, value:any) {
|
||||
var element = document.getElementById(key)
|
||||
if (element == null) {
|
||||
return
|
||||
}
|
||||
|
||||
var textValue = ""
|
||||
if (value != undefined && value != null) {
|
||||
textValue = String(value)
|
||||
}
|
||||
|
||||
if ((key == "m3u-url" || key == "xepg-url") && isClientInfoHttpURL(textValue)) {
|
||||
element.innerHTML = ""
|
||||
var anchor = document.createElement("A")
|
||||
anchor.href = textValue
|
||||
anchor.target = "_blank"
|
||||
anchor.rel = "noopener noreferrer"
|
||||
anchor.textContent = textValue
|
||||
element.appendChild(anchor)
|
||||
return
|
||||
}
|
||||
|
||||
element.innerHTML = textValue
|
||||
}
|
||||
|
||||
function createLayout() {
|
||||
|
||||
// Client Info
|
||||
@@ -848,11 +1091,10 @@ function createLayout() {
|
||||
var keys = getObjKeys(obj);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
|
||||
if (document.getElementById(keys[i])) {
|
||||
document.getElementById(keys[i]).innerHTML = obj[keys[i]];
|
||||
}
|
||||
setClientInfoValue(keys[i], obj[keys[i]])
|
||||
|
||||
}
|
||||
renderStatusCards()
|
||||
|
||||
if (!document.getElementById("main-menu")) {
|
||||
return
|
||||
@@ -889,13 +1131,32 @@ function createLayout() {
|
||||
|
||||
}
|
||||
|
||||
if (ACTIVE_MENU_ID.length > 0 && document.getElementById(ACTIVE_MENU_ID)) {
|
||||
setActiveMenu(ACTIVE_MENU_ID)
|
||||
}
|
||||
|
||||
var content = document.getElementById("content")
|
||||
var menu = document.getElementById("main-menu")
|
||||
if (ACTIVE_MENU_ID.length == 0 && content != null && menu != null) {
|
||||
|
||||
if (content.innerHTML.replace(/\\s/g, "").length == 0) {
|
||||
var firstItem = menu.getElementsByTagName("LI")[0]
|
||||
if (firstItem != undefined) {
|
||||
firstItem.click()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
function openThisMenu(element) {
|
||||
var id = element.id
|
||||
var content:ShowContent = new ShowContent(id)
|
||||
setActiveMenu(id)
|
||||
content.show()
|
||||
closeLayoutMenuIfMobile()
|
||||
calculateWrapperHeight()
|
||||
|
||||
return
|
||||
|
||||
@@ -18,6 +18,7 @@ class Server {
|
||||
if (this.cmd != "updateLog") {
|
||||
showElement("loading", true)
|
||||
UNDO = new Object()
|
||||
setConnectionState("busy")
|
||||
}
|
||||
|
||||
switch(window.location.protocol) {
|
||||
@@ -29,13 +30,67 @@ class Server {
|
||||
break
|
||||
}
|
||||
|
||||
var url = this.protocol + window.location.hostname + ":" + window.location.port + "/data/" + "?Token=" + getCookie("Token")
|
||||
var wsHost:string = window.location.host
|
||||
if (wsHost == undefined || wsHost.length < 1) {
|
||||
wsHost = window.location.hostname
|
||||
}
|
||||
var url = this.protocol + wsHost + "/data/" + "?Token=" + getCookie("Token")
|
||||
|
||||
data["cmd"] = this.cmd
|
||||
var requestCmd:string = data["cmd"]
|
||||
var ws = new WebSocket(url)
|
||||
var isLogUpdate:boolean = data["cmd"] == "updateLog"
|
||||
var responseReceived:boolean = false
|
||||
var requestFinished:boolean = false
|
||||
var timeoutMs:number = 12000
|
||||
var requestTimeout:number
|
||||
|
||||
var finishRequest = function(state:string, responseSuccess:boolean = false):void {
|
||||
if (requestFinished == true) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFinished = true
|
||||
SERVER_CONNECTION = false
|
||||
window.clearTimeout(requestTimeout)
|
||||
|
||||
if (responseSuccess == true) {
|
||||
if (state == "online") {
|
||||
WS_FAILURE_COUNT = 0
|
||||
}
|
||||
} else {
|
||||
WS_FAILURE_COUNT++
|
||||
}
|
||||
|
||||
if (isLogUpdate == false) {
|
||||
showElement("loading", false)
|
||||
}
|
||||
|
||||
if (state != "") {
|
||||
setConnectionState(state)
|
||||
}
|
||||
}
|
||||
|
||||
requestTimeout = window.setTimeout(function() {
|
||||
console.log("Websocket request timed out.")
|
||||
var timeoutState:string = "offline"
|
||||
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
|
||||
timeoutState = "idle"
|
||||
}
|
||||
finishRequest(timeoutState, false)
|
||||
try {
|
||||
ws.close()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}, timeoutMs)
|
||||
|
||||
ws.onopen = function() {
|
||||
|
||||
WS_AVAILABLE = true
|
||||
if (data["cmd"] != "updateLog") {
|
||||
setConnectionState("busy")
|
||||
}
|
||||
|
||||
console.log("REQUEST (JS):");
|
||||
console.log(data)
|
||||
@@ -50,9 +105,13 @@ class Server {
|
||||
ws.onerror = function(e) {
|
||||
|
||||
console.log("No websocket connection to xTeVe could be established. Check your network configuration.")
|
||||
SERVER_CONNECTION = false
|
||||
var errorState:string = "offline"
|
||||
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
|
||||
errorState = "idle"
|
||||
}
|
||||
finishRequest(errorState, false)
|
||||
|
||||
if (WS_AVAILABLE == false) {
|
||||
if (WS_AVAILABLE == false && isLogUpdate == false && requestCmd != "getServerConfig") {
|
||||
alert("No websocket connection to xTeVe could be established. Check your network configuration.")
|
||||
}
|
||||
|
||||
@@ -60,9 +119,8 @@ class Server {
|
||||
|
||||
|
||||
ws.onmessage = function (e) {
|
||||
|
||||
SERVER_CONNECTION = false
|
||||
showElement("loading", false)
|
||||
responseReceived = true
|
||||
finishRequest("online", true)
|
||||
|
||||
console.log("RESPONSE:");
|
||||
var response = JSON.parse(e.data);
|
||||
@@ -74,6 +132,7 @@ class Server {
|
||||
}
|
||||
|
||||
if (response["status"] == false) {
|
||||
setConnectionState("offline")
|
||||
|
||||
alert(response["err"])
|
||||
|
||||
@@ -136,10 +195,24 @@ class Server {
|
||||
|
||||
}
|
||||
|
||||
ws.onclose = function() {
|
||||
if (responseReceived == true) {
|
||||
return
|
||||
}
|
||||
|
||||
var closeState:string = "offline"
|
||||
if (isLogUpdate == true && WS_FAILURE_COUNT < 2) {
|
||||
closeState = "idle"
|
||||
}
|
||||
finishRequest(closeState, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var WS_FAILURE_COUNT:number = 0
|
||||
|
||||
function getCookie(name) {
|
||||
var value = "; " + document.cookie;
|
||||
var parts = value.split("; " + name + "=");
|
||||
|
||||
@@ -75,6 +75,35 @@ class SettingsCategory {
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "plex.url":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.plexURL.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "plex.url", data)
|
||||
input.setAttribute("placeholder", "{{.settings.plexURL.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "plex.token":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.plexToken.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("password", "plex.token", data)
|
||||
input.setAttribute("placeholder", "{{.settings.plexToken.placeholder}}")
|
||||
input.setAttribute("autocomplete", "off")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "buffer.timeout":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.bufferTimeout.title}}" + ":"
|
||||
@@ -286,10 +315,40 @@ class SettingsCategory {
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
// Select
|
||||
case "tuner":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
|
||||
case "use_plexAPI":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.usePlexAPI.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
// Select
|
||||
case "xepg.missing.epg.mode":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.xepgMissingEPGMode.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["{{.settings.xepgMissingEPGMode.info_strict}}", "{{.settings.xepgMissingEPGMode.info_relaxed}}"]
|
||||
var values:any[] = ["strict", "relaxed"]
|
||||
|
||||
var select = content.createSelect(text, values, data, settingsKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(select)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "tuner":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.tuner.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text = new Array()
|
||||
@@ -454,6 +513,14 @@ class SettingsCategory {
|
||||
text = "{{.settings.userAgent.description}}"
|
||||
break
|
||||
|
||||
case "plex.url":
|
||||
text = "{{.settings.plexURL.description}}"
|
||||
break
|
||||
|
||||
case "plex.token":
|
||||
text = "{{.settings.plexToken.description}}"
|
||||
break
|
||||
|
||||
case "ffmpeg.path":
|
||||
text = "{{.settings.ffmpegPath.description}}"
|
||||
break
|
||||
@@ -486,6 +553,10 @@ class SettingsCategory {
|
||||
text = "{{.settings.api.description}}"
|
||||
break
|
||||
|
||||
case "use_plexAPI":
|
||||
text = "{{.settings.usePlexAPI.description}}"
|
||||
break
|
||||
|
||||
case "files.update":
|
||||
text = "{{.settings.filesUpdate.description}}"
|
||||
break
|
||||
@@ -494,12 +565,16 @@ class SettingsCategory {
|
||||
text = "{{.settings.cacheImages.description}}"
|
||||
break
|
||||
|
||||
case "xepg.replace.missing.images":
|
||||
text = "{{.settings.replaceEmptyImages.description}}"
|
||||
break
|
||||
case "xepg.replace.missing.images":
|
||||
text = "{{.settings.replaceEmptyImages.description}}"
|
||||
break
|
||||
|
||||
case "udpxy":
|
||||
text = "{{.settings.udpxy.description}}"
|
||||
case "xepg.missing.epg.mode":
|
||||
text = "{{.settings.xepgMissingEPGMode.description}}"
|
||||
break
|
||||
|
||||
case "udpxy":
|
||||
text = "{{.settings.udpxy.description}}"
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
3
xteve.go
3
xteve.go
@@ -39,7 +39,8 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
|
||||
const Name = "xTeVe"
|
||||
|
||||
// Version : Version, die Build Nummer wird in der main func geparst.
|
||||
const Version = "2.2.0.0200"
|
||||
// Can be overwritten at build time: -ldflags "-X main.Version=..."
|
||||
var Version = "2.2.0.0200"
|
||||
|
||||
// DBVersion : Datanbank Version
|
||||
const DBVersion = "2.1.0"
|
||||
|
||||
Reference in New Issue
Block a user