Compare commits
127 Commits
2.0.1.0010
...
master
| 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 | |||
|
|
0e999b85b9 | ||
|
|
e4b3667fe0 | ||
|
|
e44eff0645 | ||
|
|
884b427754 | ||
|
|
25bad13800 | ||
|
|
d2456b0506 | ||
|
|
19b9a259b1 | ||
|
|
441cefa3a4 | ||
|
|
014f5b7218 | ||
|
|
0310b7e738 | ||
|
|
6d890cfd33 | ||
|
|
410cc3648f | ||
|
|
07f9cac3c5 | ||
|
|
f43ce0f7c5 | ||
|
|
db59f7ef37 | ||
|
|
b407ec5bf0 | ||
|
|
d8fc1aea97 | ||
|
|
38333d65cb | ||
|
|
a6a9b90937 | ||
|
|
2b3fe6a09d | ||
|
|
a09eca59a7 | ||
|
|
0df3b4d755 | ||
|
|
5552514a1f | ||
|
|
5fceb1d34f | ||
|
|
fb5d0a3904 | ||
|
|
28fe4dcf1c | ||
|
|
bc307a7cd4 | ||
|
|
fd3024d7ff | ||
|
|
d3f6725ba6 | ||
|
|
c266012db3 | ||
|
|
d602b60710 | ||
|
|
67b7ba6df9 | ||
|
|
71dfe91272 | ||
|
|
534510a4ec | ||
|
|
2d10fc9313 | ||
|
|
a63a9c0d8f | ||
|
|
dd911d6e5d | ||
|
|
c1970a8393 | ||
|
|
493d612d52 | ||
|
|
4fc4330a94 | ||
|
|
a683533824 | ||
|
|
4b9f5826cf | ||
|
|
ca49d70910 | ||
|
|
1b425018d4 | ||
|
|
87b36c283b | ||
|
|
6da26ff4fb | ||
|
|
cd08985e79 | ||
|
|
dc04519229 | ||
|
|
c4ad96b715 | ||
|
|
45b5e602bb | ||
|
|
1cefbf022d | ||
|
|
d5328f6b1a | ||
|
|
91b80bc8bb | ||
|
|
aa763726a3 | ||
|
|
66c01dd1fb | ||
|
|
03e1abbe90 | ||
|
|
469581e280 | ||
|
|
019c98996a | ||
|
|
36db927794 | ||
|
|
f0a49788cc | ||
|
|
72767d7dbd | ||
|
|
8eecbf2b78 | ||
|
|
1a1e37fe15 | ||
|
|
08f6fb60e3 | ||
|
|
eded490ac7 | ||
|
|
ed770b9dbc | ||
|
|
6129b4911a | ||
|
|
477c5f30c1 | ||
|
|
65ddc6f301 | ||
|
|
3ef95c1950 | ||
|
|
3a3798cd2d | ||
|
|
72eb7fb599 | ||
|
|
4b969b8cee | ||
|
|
3d9266dabe | ||
|
|
48218cda50 | ||
|
|
dc42afcd05 | ||
|
|
20e5e1b545 | ||
|
|
11dd830110 | ||
|
|
7c87d1d5bd | ||
|
|
6cdd44357b | ||
|
|
ad992eb615 | ||
|
|
11453c6053 | ||
|
|
81e8ae33d7 | ||
|
|
f3be0fca47 | ||
|
|
1c1c89cd74 | ||
|
|
3d73dba422 | ||
|
|
c6e74fe11c | ||
|
|
18dba46c02 | ||
|
|
efa55b39a9 | ||
|
|
717fa68b7e | ||
|
|
878531ff79 | ||
|
|
792fd9a373 | ||
|
|
a1ec0287ef | ||
|
|
a79e824ef8 | ||
|
|
c843f424fe | ||
|
|
5c6637c048 | ||
|
|
1062e072d6 | ||
|
|
d831a099f0 | ||
|
|
a06baef4d3 | ||
|
|
f9d1a45bbd |
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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
.DS_Store
|
||||
demo
|
||||
dev
|
||||
compiler
|
||||
files
|
||||
update_xteve*.sh
|
||||
|
||||
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"]
|
||||
65
README.md
65
README.md
@@ -31,7 +31,7 @@ Documentation for setup and configuration is [here](https://github.com/xteve-pro
|
||||
* Merge external M3U files
|
||||
* Merge external XMLTV files
|
||||
* Automatic M3U and XMLTV update
|
||||
* M3U und XMLTV export
|
||||
* M3U and XMLTV export
|
||||
|
||||
#### Channel management
|
||||
* Filtering streams
|
||||
@@ -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
|
||||
|
||||
@@ -81,15 +134,10 @@ Including:
|
||||
- Crond: Daemon to execute scheduled commands
|
||||
- Perl: Programming language
|
||||
|
||||
## Beta version
|
||||
|
||||
New features are first available in the beta version and will be added to the master branch after successful testing
|
||||
If you prefer to use the beta version, you can always switch between master and beta branch.
|
||||
|
||||
---
|
||||
|
||||
### xTeVe Beta branch
|
||||
New features and bug fixes are only available in beta brunch. Only after successful testing, they are merged into the master branch.
|
||||
New features and bug fixes are only available in beta branch. Only after successful testing are they are merged into the master branch.
|
||||
|
||||
**It is not recommended to use the beta version in a production system.**
|
||||
|
||||
@@ -122,7 +170,7 @@ When the branch is changed, an update is only performed if there is a new versio
|
||||
## Build from source code [Go / Golang]
|
||||
|
||||
#### Requirements
|
||||
* [Go](https://golang.org) (go1.12.4 or newer)
|
||||
* [Go](https://golang.org) (go1.16.2 or newer)
|
||||
|
||||
#### Dependencies
|
||||
* [go-ssdp](https://github.com/koron/go-ssdp)
|
||||
@@ -161,4 +209,3 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,82 @@
|
||||
#### 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.
|
||||
|
||||
#### 2.1.1.0115-beta
|
||||
```diff
|
||||
+ GZIP compression for xteve.xml file. (http://xteve.ip:34400/xmltv/xteve.xml.gz)
|
||||
- Removed protocol setting for reverse proxy. HTTPS can also be configured in the proxy, where it makes more sense.
|
||||
```
|
||||
|
||||
#### 2.1.0.0106-beta
|
||||
```diff
|
||||
+ User-Agent is now also used by VLC and FFmpeg.
|
||||
```
|
||||
|
||||
#### 2.1.0.0105-beta
|
||||
```diff
|
||||
+ Fixed wrong buffer value in log
|
||||
+ New setting: URL protocol for M3U and XML file
|
||||
+ Add xml tag premiere to xteve.xml
|
||||
```
|
||||
|
||||
#### 2.1.0.0101-beta
|
||||
```diff
|
||||
+ Reverse proxy fix
|
||||
```
|
||||
|
||||
#### 2.0.3.0042-beta
|
||||
**Version 2.0.3.0042 changes the settings.json.**
|
||||
Settings from the current beta can not be used for the current master version 2.0.3
|
||||
- New default options for VLC and FFmpeg
|
||||
- VLC and FFmpeg log entries in the xTeVe log
|
||||
- Less CPU load with VLC and FFmpeg
|
||||
|
||||
#### 2.0.3.0035-beta
|
||||
```diff
|
||||
+ FFmpeg support
|
||||
+ VLC support
|
||||
```
|
||||
**Version 2.0.3.0035 changes the settings.json.**
|
||||
Settings from the current beta can not be used for the current master version 2.0.3
|
||||
|
||||
#### 2.0.2.0024-beta
|
||||
```diff
|
||||
+ Improved monitoring of the buffer process
|
||||
+ Update the XEPG database a bit faster
|
||||
```
|
||||
|
||||
##### Fixes
|
||||
- Error message if filter rule is missing
|
||||
- Channels are lost when saving again (Mapping)
|
||||
- Plex log, invalid source: IPTV
|
||||
|
||||
#### 2.0.1.0012-beta
|
||||
```diff
|
||||
+ Add support for "video/m2ts" video streams (Pull request #14)
|
||||
```
|
||||
#### 2.0.1.0011-beta
|
||||
```diff
|
||||
+ Original group title is shown in the Mapping Editor
|
||||
```
|
||||
##### Fixes
|
||||
- incorrect original-air-date
|
||||
|
||||
#### 2.0.1.0010-beta
|
||||
```diff
|
||||
+ Set timestamp to <episode-num system="original-air-date">
|
||||
|
||||
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}" "$@"
|
||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module xteve
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
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
|
||||
)
|
||||
28
go.sum
Normal file
28
go.sum
Normal file
@@ -0,0 +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="/web/" 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,9 +21,9 @@ 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.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent"));
|
||||
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"));
|
||||
function showPopUpElement(elm) {
|
||||
@@ -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];
|
||||
@@ -226,8 +354,9 @@ function createSearchObj() {
|
||||
SEARCH_MAPPING = new Object();
|
||||
var data = SERVER["xepg"]["epgMapping"];
|
||||
var channels = getObjKeys(data);
|
||||
var channelKeys = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title"];
|
||||
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]) {
|
||||
@@ -240,7 +369,15 @@ function createSearchObj() {
|
||||
}
|
||||
}
|
||||
else {
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " ";
|
||||
if (key == "x-xmltv-file") {
|
||||
var xmltvFile = getValueFromProviderFile(data[id][key], "xmltv", "name");
|
||||
if (xmltvFile != undefined) {
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + xmltvFile + " ";
|
||||
}
|
||||
}
|
||||
else {
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " ";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -252,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 = "";
|
||||
@@ -261,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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
@@ -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);
|
||||
|
||||
@@ -2,7 +2,7 @@ var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
@@ -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);
|
||||
@@ -127,7 +134,7 @@ var Content = /** @class */ (function () {
|
||||
var cell = new Cell();
|
||||
cell.child = true;
|
||||
cell.childType = "P";
|
||||
if (SERVER["settings"]["buffer"] == true) {
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
cell.value = data[key]["tuner"];
|
||||
}
|
||||
else {
|
||||
@@ -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);
|
||||
@@ -901,7 +1344,7 @@ function openPopUp(dataType, element) {
|
||||
input.setAttribute("placeholder", "{{.playlist.fileM3U.placeholder}}");
|
||||
content.appendRow("{{.playlist.fileM3U.title}}", input);
|
||||
// Tuner
|
||||
if (SERVER["settings"]["buffer"] == true) {
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
var text = new Array();
|
||||
var values = new Array();
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
@@ -971,7 +1414,7 @@ function openPopUp(dataType, element) {
|
||||
input.setAttribute("placeholder", "{{.playlist.fileHDHR.placeholder}}");
|
||||
content.appendRow("{{.playlist.fileHDHR.title}}", input);
|
||||
// Tuner
|
||||
if (SERVER["settings"]["buffer"] == true) {
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
var text = new Array();
|
||||
var values = new Array();
|
||||
for (var i = 1; i <= 100; i++) {
|
||||
@@ -1253,6 +1696,12 @@ function openPopUp(dataType, element) {
|
||||
}
|
||||
content.appendRow("{{.mapping.channelName.title}}", input);
|
||||
content.description(data["name"]);
|
||||
// Beschreibung
|
||||
var dbKey = "x-description";
|
||||
var input = content.createInput("text", dbKey, data[dbKey]);
|
||||
input.setAttribute("placeholder", "{{.mapping.description.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
content.appendRow("{{.mapping.description.title}}", input);
|
||||
// Aktualisierung des Kanalnamens
|
||||
if (data.hasOwnProperty("_uuid.key")) {
|
||||
if (data["_uuid.key"] != "") {
|
||||
@@ -1279,7 +1728,7 @@ function openPopUp(dataType, element) {
|
||||
// Erweitern der EPG Kategorie
|
||||
var dbKey = "x-category";
|
||||
var text = ["-", "Kids (Emby only)", "News", "Movie", "Series", "Sports"];
|
||||
var values = ["-", "Kids", "News", "Movie", "Series", "Sports"];
|
||||
var values = ["", "Kids", "News", "Movie", "Series", "Sports"];
|
||||
var select = content.createSelect(text, values, data[dbKey], dbKey);
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
content.appendRow("{{.mapping.epgCategory.title}}", select);
|
||||
@@ -1288,6 +1737,9 @@ function openPopUp(dataType, element) {
|
||||
var input = content.createInput("text", dbKey, data[dbKey]);
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
content.appendRow("{{.mapping.m3uGroupTitle.title}}", input);
|
||||
if (data["group-title"] != undefined) {
|
||||
content.description(data["group-title"]);
|
||||
}
|
||||
// XMLTV Datei
|
||||
var dbKey = "x-xmltv-file";
|
||||
var xmlFile = data[dbKey];
|
||||
|
||||
@@ -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 + "=");
|
||||
|
||||
@@ -2,7 +2,7 @@ var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
@@ -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}}" + ":";
|
||||
@@ -85,6 +108,50 @@ var SettingsCategory = /** @class */ (function () {
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "ffmpeg.path":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.ffmpegPath.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "ffmpeg.path", data);
|
||||
input.setAttribute("placeholder", "{{.settings.ffmpegPath.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "ffmpeg.options":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.ffmpegOptions.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "ffmpeg.options", data);
|
||||
input.setAttribute("placeholder", "{{.settings.ffmpegOptions.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "vlc.path":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.vlcPath.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "vlc.path", data);
|
||||
input.setAttribute("placeholder", "{{.settings.vlcPath.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "vlc.options":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.vlcOptions.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "vlc.options", data);
|
||||
input.setAttribute("placeholder", "{{.settings.vlcOptions.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
// Checkboxen
|
||||
case "authentication.web":
|
||||
var tdLeft = document.createElement("TD");
|
||||
@@ -185,17 +252,6 @@ var SettingsCategory = /** @class */ (function () {
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "buffer":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createCheckbox(settingsKey);
|
||||
input.checked = data;
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "api":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.api.title}}" + ":";
|
||||
@@ -207,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}}" + ":";
|
||||
@@ -260,6 +339,29 @@ var SettingsCategory = /** @class */ (function () {
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
case "buffer":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var text = ["{{.settings.streamBuffering.info_false}}", "xTeVe: ({{.settings.streamBuffering.info_xteve}})", "FFmpeg: ({{.settings.streamBuffering.info_ffmpeg}})", "VLC: ({{.settings.streamBuffering.info_vlc}})"];
|
||||
var values = ["-", "xteve", "ffmpeg", "vlc"];
|
||||
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 "udpxy":
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.udpxy.title}}" + ":";
|
||||
var tdRight = document.createElement("TD");
|
||||
var input = content.createInput("text", "udpxy", data);
|
||||
input.setAttribute("placeholder", "{{.settings.udpxy.placeholder}}");
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'");
|
||||
tdRight.appendChild(input);
|
||||
setting.appendChild(tdLeft);
|
||||
setting.appendChild(tdRight);
|
||||
break;
|
||||
}
|
||||
return setting;
|
||||
};
|
||||
@@ -308,6 +410,24 @@ 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;
|
||||
case "ffmpeg.options":
|
||||
text = "{{.settings.ffmpegOptions.description}}";
|
||||
break;
|
||||
case "vlc.path":
|
||||
text = "{{.settings.vlcPath.description}}";
|
||||
break;
|
||||
case "vlc.options":
|
||||
text = "{{.settings.vlcOptions.description}}";
|
||||
break;
|
||||
case "epgSource":
|
||||
text = "{{.settings.epgSource.description}}";
|
||||
break;
|
||||
@@ -320,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;
|
||||
@@ -329,6 +452,12 @@ 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;
|
||||
default:
|
||||
text = "";
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"mainMenu": {
|
||||
"item":{
|
||||
"mainMenu":
|
||||
{
|
||||
"item":
|
||||
{
|
||||
"playlist": "Playlist",
|
||||
"pmsID": "PMS ID",
|
||||
"filter": "Filter",
|
||||
@@ -11,7 +13,8 @@
|
||||
"log": "Log",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"headline": {
|
||||
"headline":
|
||||
{
|
||||
"playlist": "Local or remote playlists",
|
||||
"filter": "Filter playlist",
|
||||
"xmltv": "Local or remote XMLTV files",
|
||||
@@ -22,19 +25,23 @@
|
||||
"logout": "Logout"
|
||||
}
|
||||
},
|
||||
"confirm":{
|
||||
"confirm":
|
||||
{
|
||||
"restore": "All data will be replaced with those from the backup. Should the files be restored?"
|
||||
},
|
||||
"alert": {
|
||||
"alert":
|
||||
{
|
||||
"fileLoadingError": "File couldn't be loaded",
|
||||
"invalidChannelNumber": "Invalid channel number",
|
||||
"missingInput": "Missing input"
|
||||
},
|
||||
"button":{
|
||||
"button":
|
||||
{
|
||||
"back": "Back",
|
||||
"backup": "Backup",
|
||||
"bulkEdit": "Bulk Edit",
|
||||
"cancel": "Cancel",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"done": "Done",
|
||||
"login": "Login",
|
||||
@@ -48,58 +55,70 @@
|
||||
"resetLogs": "Reset Logs",
|
||||
"uploadLogo": "Upload Logo"
|
||||
},
|
||||
"filter": {
|
||||
"table": {
|
||||
"filter":
|
||||
{
|
||||
"table":
|
||||
{
|
||||
"name": "Filter Name",
|
||||
"type": "Filter Type",
|
||||
"filter": "Filter"
|
||||
},
|
||||
"custom": "Custom",
|
||||
"group": "Group",
|
||||
"name": {
|
||||
"name":
|
||||
{
|
||||
"title": "Filter Name",
|
||||
"placeholder": "Filter name",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"description":
|
||||
{
|
||||
"title": "Description",
|
||||
"placeholder": "Description",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"type":
|
||||
{
|
||||
"title": "Type",
|
||||
"groupTitle": "Group Title",
|
||||
"customFilter": "Custom Filter"
|
||||
},
|
||||
"caseSensitive": {
|
||||
"caseSensitive":
|
||||
{
|
||||
"title": "Case Sensitive",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"filterRule": {
|
||||
"filterRule":
|
||||
{
|
||||
"title": "Filter Rule",
|
||||
"placeholder": "Sport {HD} !{ES,IT}",
|
||||
"description": ""
|
||||
},
|
||||
"filterGroup": {
|
||||
"filterGroup":
|
||||
{
|
||||
"title": "Group Title",
|
||||
"placeholder": "",
|
||||
"description": "Select a M3U group. (Counter)<br>Changing the group title in the M3U invalidates the filter."
|
||||
},
|
||||
"include": {
|
||||
"include":
|
||||
{
|
||||
"title": "Include",
|
||||
"placeholder": "FHD,UHD",
|
||||
"description": "Channel name must include.<br>(Comma separated) Comma means or"
|
||||
},
|
||||
"exclude": {
|
||||
"exclude":
|
||||
{
|
||||
"title": "Exclude",
|
||||
"placeholder": "ES,IT",
|
||||
"description": "Channel name must not contain.<br>(Comma separated) Comma means or"
|
||||
}
|
||||
|
||||
},
|
||||
"playlist": {
|
||||
"table": {
|
||||
"playlist":
|
||||
{
|
||||
"table":
|
||||
{
|
||||
"playlist": "Playlist",
|
||||
"tuner": "Tuner",
|
||||
"lastUpdate": "Last Update",
|
||||
@@ -110,124 +129,156 @@
|
||||
"tvgID": "tvg-id",
|
||||
"uniqueID": "Unique ID"
|
||||
},
|
||||
"playlistType": {
|
||||
"playlistType":
|
||||
{
|
||||
"title": "Playlist type",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"type": {
|
||||
"type":
|
||||
{
|
||||
"title": "Type",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"name": {
|
||||
"name":
|
||||
{
|
||||
"title": "Name",
|
||||
"placeholder": "Playlist name",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"description":
|
||||
{
|
||||
"title": "Description",
|
||||
"placeholder": "Description",
|
||||
"description": ""
|
||||
},
|
||||
"fileM3U": {
|
||||
"fileM3U":
|
||||
{
|
||||
"title": "M3U File",
|
||||
"placeholder": "File path or URL of the M3U",
|
||||
"description": ""
|
||||
},
|
||||
"fileHDHR": {
|
||||
"fileHDHR":
|
||||
{
|
||||
"title": "HDHomeRun IP",
|
||||
"placeholder": "IP address and port (192.168.1.10:5004)",
|
||||
"description": ""
|
||||
},
|
||||
"tuner": {
|
||||
"tuner":
|
||||
{
|
||||
"title": "Tuner / Streams",
|
||||
"placeholder": "",
|
||||
"description": "Number of parallel connections that can be established to the provider. <br>Only available with activated buffer.<br>New settings will only be applied after quitting all streams."
|
||||
}
|
||||
},
|
||||
"xmltv": {
|
||||
"table": {
|
||||
"xmltv":
|
||||
{
|
||||
"table":
|
||||
{
|
||||
"guide": "Guide",
|
||||
"lastUpdate": "Last Update",
|
||||
"availability": "Availability",
|
||||
"channels": "Channels",
|
||||
"programs": "Programs"
|
||||
},
|
||||
"name": {
|
||||
"name":
|
||||
{
|
||||
"title": "Name",
|
||||
"placeholder": "Guide name",
|
||||
"description": ""
|
||||
},
|
||||
"description": {
|
||||
"description":
|
||||
{
|
||||
"title": "Description",
|
||||
"placeholder": "Description",
|
||||
"description": ""
|
||||
},
|
||||
"fileXMLTV": {
|
||||
"fileXMLTV":
|
||||
{
|
||||
"title": "XMLTV File",
|
||||
"placeholder": "File path or URL of the XMLTV",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"mapping": {
|
||||
"table": {
|
||||
"mapping":
|
||||
{
|
||||
"table":
|
||||
{
|
||||
"chNo": "Ch. No.",
|
||||
"logo": "Logo",
|
||||
"channelName": "Channel Name",
|
||||
"playlist": "Playlist",
|
||||
"groupTitle": "Group Title",
|
||||
"xmltvFile": "XMLTV File",
|
||||
"xmltvID": "XMLTV ID"
|
||||
"xmltvID": "XMLTV ID",
|
||||
"edit": "Edit"
|
||||
},
|
||||
"active": {
|
||||
"active":
|
||||
{
|
||||
"title": "Active",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"channelName": {
|
||||
"channelName":
|
||||
{
|
||||
"title": "Channel Name",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"updateChannelName": {
|
||||
"description":
|
||||
{
|
||||
"title": "Channel Description",
|
||||
"placeholder": "Used by the Dummy as an XML description",
|
||||
"description": ""
|
||||
},
|
||||
"updateChannelName":
|
||||
{
|
||||
"title": "Update Channel Name",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"channelLogo": {
|
||||
"channelLogo":
|
||||
{
|
||||
"title": "Logo URL",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"updateChannelLogo": {
|
||||
"updateChannelLogo":
|
||||
{
|
||||
"title": "Update Channel Logo",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"epgCategory": {
|
||||
"epgCategory":
|
||||
{
|
||||
"title": "EPG Category",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"m3uGroupTitle": {
|
||||
"m3uGroupTitle":
|
||||
{
|
||||
"title": "Group Title (xteve.m3u)",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"xmltvFile": {
|
||||
"xmltvFile":
|
||||
{
|
||||
"title": "XMLTV File",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"xmltvChannel": {
|
||||
"xmltvChannel":
|
||||
{
|
||||
"title": "XMLTV Channel",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"table": {
|
||||
"users":
|
||||
{
|
||||
"table":
|
||||
{
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"web": "WEB",
|
||||
@@ -236,185 +287,286 @@
|
||||
"xml": "XML",
|
||||
"api": "API"
|
||||
},
|
||||
"username": {
|
||||
"username":
|
||||
{
|
||||
"title": "Username",
|
||||
"placeholder": "Username",
|
||||
"description": ""
|
||||
},
|
||||
"password": {
|
||||
"password":
|
||||
{
|
||||
"title": "Password",
|
||||
"placeholder": "Password",
|
||||
"description": ""
|
||||
},
|
||||
"confirm": {
|
||||
"confirm":
|
||||
{
|
||||
"title": "Confirm",
|
||||
"placeholder": "Password confirm",
|
||||
"description": ""
|
||||
},
|
||||
"web": {
|
||||
"web":
|
||||
{
|
||||
"title": "Web Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"pms": {
|
||||
"pms":
|
||||
{
|
||||
"title": "PMS Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"m3u": {
|
||||
"m3u":
|
||||
{
|
||||
"title": "M3U Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"xml": {
|
||||
"xml":
|
||||
{
|
||||
"title": "XML Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
},
|
||||
"api": {
|
||||
"api":
|
||||
{
|
||||
"title": "API Access",
|
||||
"placeholder": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"category": {
|
||||
"settings":
|
||||
{
|
||||
"category":
|
||||
{
|
||||
"general": "General",
|
||||
"files": "Files",
|
||||
"streaming": "Streaming",
|
||||
"backup": "Backup",
|
||||
"authentication": "Authentication"
|
||||
},
|
||||
"update": {
|
||||
"update":
|
||||
{
|
||||
"title": "Schedule for updating (Playlist, XMLTV, Backup)",
|
||||
"placeholder": "0000,1000,2000",
|
||||
"description": "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated."
|
||||
"description": "Time in 24 hour format (0800 = 8:00 am). More times can be entered comma separated. Leave this field empty if no updates are to be carried out."
|
||||
},
|
||||
"api": {
|
||||
"api":
|
||||
{
|
||||
"title": "API Interface",
|
||||
"description": "Via API interface it is possible to send commands to xTeVe. API documentation is <a href='https://github.com/xteve-project/xTeVe-Documentation/blob/master/en/configuration.md#api'>here</a>"
|
||||
},
|
||||
"epgSource": {
|
||||
"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",
|
||||
"description": "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
|
||||
},
|
||||
"tuner":{
|
||||
"tuner":
|
||||
{
|
||||
"title": "Number of Tuners",
|
||||
"description": "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
|
||||
},
|
||||
"filesUpdate": {
|
||||
"filesUpdate":
|
||||
{
|
||||
"title": "Updates all files at startup",
|
||||
"description": "Updates all playlists, tuner and XMLTV files at startup."
|
||||
},
|
||||
"cacheImages": {
|
||||
"title": "Image caching",
|
||||
"cacheImages":
|
||||
{
|
||||
"title": "Image Caching",
|
||||
"description": "All images from the XMLTV file are cached, allowing faster rendering of the grid in the client.<br>Downloading the images may take a while and will be done in the background."
|
||||
},
|
||||
"replaceEmptyImages": {
|
||||
"replaceEmptyImages":
|
||||
{
|
||||
"title": "Replace missing program images",
|
||||
"description": "If the poster in the XMLTV program is missing, the channel logo will be used."
|
||||
},
|
||||
"xteveAutoUpdate": {
|
||||
"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",
|
||||
"description": "If a new version of xTeVe is available, it will be automatically installed. The updates are downloaded from GitHub."
|
||||
},
|
||||
"streamBuffering": {
|
||||
"streamBuffering":
|
||||
{
|
||||
"title": "Stream Buffer",
|
||||
"description": "- The stream is passed from xTeVe to Plex / Emby / M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support<br>- Re-streaming<br>- Separate tuner limit for each playlist"
|
||||
"description": "Functions of the buffer:<br>- The stream is passed from xTeVe, FFmpeg or VLC to Plex, Emby or M3U Player<br>- Small jerking of the streams can be compensated<br>- HLS / M3U8 support<br>- RTP / RTPS support (only FFmpeg or VLC)<br>- Re-streaming<br>- Separate tuner limit for each playlist",
|
||||
"info_false": "No Buffer (Client connects to the streaming server)",
|
||||
"info_xteve": "xTeVe connects to the streaming server",
|
||||
"info_ffmpeg": "FFmpeg connects to the streaming server",
|
||||
"info_vlc": "VLC connects to the streaming server"
|
||||
|
||||
},
|
||||
"bufferSize": {
|
||||
"udpxy":
|
||||
{
|
||||
"title": "UDPxy address",
|
||||
"description": "The address of your UDPxy server. If set, and the channel URLs in the m3u is multicast, xTeVe will rewrite it so that it is accessed via the UDPxy service.",
|
||||
"placeholder": "host:port"
|
||||
},
|
||||
"ffmpegPath":
|
||||
{
|
||||
"title": "FFmpeg Binary Path",
|
||||
"description": "Path to FFmpeg binary.",
|
||||
"placeholder": "/path/to/ffmpeg"
|
||||
},
|
||||
"ffmpegOptions":
|
||||
{
|
||||
"title": "FFmpeg Options",
|
||||
"description": "FFmpeg options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings.",
|
||||
"placeholder": "Leave blank to set default settings"
|
||||
},
|
||||
"vlcPath":
|
||||
{
|
||||
"title": "VLC / CVLC Binary Path",
|
||||
"description": "Path to VLC / CVLC binary.",
|
||||
"placeholder": "/path/to/cvlc"
|
||||
},
|
||||
"vlcOptions":
|
||||
{
|
||||
"title": "VLC / CVLC Options",
|
||||
"description": "VLC / CVLC options.<br>Only change if you know what you are doing.<br>Leave blank to set default settings.",
|
||||
"placeholder": "Leave blank to set default settings"
|
||||
},
|
||||
"bufferSize":
|
||||
{
|
||||
"title": "Buffer Size",
|
||||
"description": "Buffer size in MB.<br>M3U8: If the TS segment smaller then the buffer size, the file size of the segment is used."
|
||||
},
|
||||
"bufferTimeout": {
|
||||
"bufferTimeout":
|
||||
{
|
||||
"title": "Timeout for new client connections",
|
||||
"description": "The xTeVe buffer waits until new client connections are established. Helpful for fast channel switching. Value in milliseconds.",
|
||||
"placeholder": "100"
|
||||
},
|
||||
"userAgent": {
|
||||
"title": "User agent",
|
||||
"description": "User Agent for HTTP requests",
|
||||
"userAgent":
|
||||
{
|
||||
"title": "User Agent",
|
||||
"description": "User Agent for HTTP requests. For every HTTP connection, this value is used for the user agent. Should only be changed if xTeVe is blocked.",
|
||||
"placeholder": "xTeVe"
|
||||
},
|
||||
"backupPath": {
|
||||
"backupPath":
|
||||
{
|
||||
"title": "Location for automatic backups",
|
||||
"placeholder": "/mnt/data/backup/xteve/",
|
||||
"description": "Before any update of the provider data by the schedule, xTeVe creates a backup. The path for the automatic backups can be changed. xTeVe requires write permission for this folder."
|
||||
},
|
||||
"tempPath": {
|
||||
"tempPath":
|
||||
{
|
||||
"title": "Location for the temporary files",
|
||||
"placeholder": "/tmp/xteve/",
|
||||
"description": "Location for the buffer files."
|
||||
},
|
||||
"backupKeep": {
|
||||
"backupKeep":
|
||||
{
|
||||
"title": "Number of backups to keep",
|
||||
"description": "Number of backups to keep. Older backups are automatically deleted."
|
||||
},
|
||||
"authenticationWEB": {
|
||||
"authenticationWEB":
|
||||
{
|
||||
"title": "WEB Authentication",
|
||||
"description": "Access to the web interface only possible with credentials."
|
||||
},
|
||||
"authenticationPMS": {
|
||||
"authenticationPMS":
|
||||
{
|
||||
"title": "PMS Authentication",
|
||||
"description": "Plex requests are only possible with authentication. <br><b>Warning!!!</b> After activating this function xTeVe must be delete in the PMS DVR settings and set up again."
|
||||
},
|
||||
"authenticationM3U": {
|
||||
"authenticationM3U":
|
||||
{
|
||||
"title": "M3U Authentication",
|
||||
"description": "Downloading the xteve.m3u file via an HTTP request is only possible with authentication."
|
||||
},
|
||||
"authenticationXML": {
|
||||
"authenticationXML":
|
||||
{
|
||||
"title": "XML Authentication",
|
||||
"description": "Downloading the xteve.xml file via an HTTP request is only possible with authentication"
|
||||
},
|
||||
"authenticationAPI": {
|
||||
"authenticationAPI":
|
||||
{
|
||||
"title": "API Authentication",
|
||||
"description": "Access to the API interface is only possible with authentication."
|
||||
}
|
||||
},
|
||||
"wizard": {
|
||||
"epgSource": {
|
||||
"wizard":
|
||||
{
|
||||
"epgSource":
|
||||
{
|
||||
"title": "EPG Source",
|
||||
"description": "PMS:<br>- Use EPG data from Plex or Emby <br><br>XEPG:<br>- Use of one or more XMLTV files<br>- Channel management<br>- M3U / XMLTV export (HTTP link for IPTV apps)"
|
||||
},
|
||||
"tuner":{
|
||||
"tuner":
|
||||
{
|
||||
"title": "Number of tuners",
|
||||
"description": "Number of parallel connections that can be established to the provider.<br>Available for: Plex, Emby (HDHR), M3U (with active buffer).<br>After a change, xTeVe must be delete in the Plex / Emby DVR settings and set up again."
|
||||
},
|
||||
"m3u": {
|
||||
"m3u":
|
||||
{
|
||||
"title": "M3U Playlist",
|
||||
"placeholder": "File path or URL of the M3U",
|
||||
"description": "Local or remote playlists"
|
||||
},
|
||||
"xmltv": {
|
||||
"xmltv":
|
||||
{
|
||||
"title": "XMLTV File",
|
||||
"placeholder": "File path or URL of the XMLTV",
|
||||
"description": "Local or remote XMLTV file"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"login":
|
||||
{
|
||||
"failed": "User authentication failed",
|
||||
"headline": "Login",
|
||||
"username": {
|
||||
"username":
|
||||
{
|
||||
"title": "Username",
|
||||
"placeholder": "Username"
|
||||
},
|
||||
"password": {
|
||||
"password":
|
||||
{
|
||||
"title": "Password",
|
||||
"placeholder": "Password"
|
||||
}
|
||||
},
|
||||
"account": {
|
||||
"account":
|
||||
{
|
||||
"failed": "Password does not match",
|
||||
"headline": "Create user account",
|
||||
"username": {
|
||||
"username":
|
||||
{
|
||||
"title": "Username",
|
||||
"placeholder": "Username"
|
||||
},
|
||||
"password": {
|
||||
"password":
|
||||
{
|
||||
"title": "Password",
|
||||
"placeholder": "Password"
|
||||
},
|
||||
"confirm": {
|
||||
"confirm":
|
||||
{
|
||||
"title": "Confirm",
|
||||
"placeholder": "Confirm"
|
||||
}
|
||||
|
||||
@@ -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="/web/" 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>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"../src/internal/authentication"
|
||||
"xteve/src/internal/authentication"
|
||||
)
|
||||
|
||||
func activatedSystemAuthentication() (err error) {
|
||||
@@ -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
|
||||
|
||||
1241
src/buffer.go
1241
src/buffer.go
File diff suppressed because it is too large
Load Diff
@@ -146,3 +146,20 @@ func extractGZIP(gzipBody []byte, fileSource string) (body []byte, err error) {
|
||||
body = resB.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func compressGZIP(data *[]byte, file string) (err error) {
|
||||
|
||||
if len(file) != 0 {
|
||||
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := gzip.NewWriter(f)
|
||||
w.Write(*data)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ var System SystemStruct
|
||||
var WebScreenLog WebScreenLogStruct
|
||||
|
||||
// Settings : Inhalt der settings.json
|
||||
var Settings SettingsStrcut
|
||||
var Settings SettingsStruct
|
||||
|
||||
// Data : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
|
||||
var Data DataStruct
|
||||
@@ -29,6 +29,9 @@ var BufferInformation sync.Map
|
||||
// BufferClients : Anzahl der Clients die einen Stream über den Buffer abspielen
|
||||
var BufferClients sync.Map
|
||||
|
||||
// Lock : Lock Map
|
||||
var Lock = sync.RWMutex{}
|
||||
|
||||
// Init : Systeminitialisierung
|
||||
func Init() (err error) {
|
||||
|
||||
@@ -43,9 +46,14 @@ func Init() (err error) {
|
||||
System.ServerProtocol.M3U = "http"
|
||||
System.ServerProtocol.WEB = "http"
|
||||
System.ServerProtocol.XML = "http"
|
||||
System.DVRLimit = 480
|
||||
System.PlexChannelLimit = 480
|
||||
System.UnfilteredChannelLimit = 480
|
||||
System.Compatibility = "1.4.4"
|
||||
|
||||
// FFmpeg Default Einstellungen
|
||||
System.FFmpeg.DefaultOptions = "-hide_banner -loglevel error -i [URL] -c copy -f mpegts pipe:1"
|
||||
System.VLC.DefaultOptions = "-I dummy [URL] --sout #std{mux=ts,access=file,dst=-}"
|
||||
|
||||
// Default Logeinträge, wird später von denen aus der settings.json überschrieben. Muss gemacht werden, damit die ersten Einträge auch im Log (webUI aangezeigt werden)
|
||||
Settings.LogEntriesRAM = 500
|
||||
|
||||
@@ -91,6 +99,8 @@ func Init() (err error) {
|
||||
System.File.XML = getPlatformFile(fmt.Sprintf("%s%s.xml", System.Folder.Data, System.AppName))
|
||||
System.File.M3U = getPlatformFile(fmt.Sprintf("%s%s.m3u", System.Folder.Data, System.AppName))
|
||||
|
||||
System.Compressed.GZxml = getPlatformFile(fmt.Sprintf("%s%s.xml.gz", System.Folder.Data, System.AppName))
|
||||
|
||||
err = activatedSystemAuthentication()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -109,7 +119,7 @@ func Init() (err error) {
|
||||
|
||||
// Überprüfen ob xTeVe als root läuft
|
||||
if os.Geteuid() == 0 {
|
||||
showWarning(2010)
|
||||
showWarning(2110)
|
||||
}
|
||||
|
||||
if System.Flag.Debug > 0 {
|
||||
@@ -133,7 +143,6 @@ func Init() (err error) {
|
||||
// Bedingte Update Änderungen durchführen
|
||||
err = conditionalUpdateChanges()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,7 +230,8 @@ func StartSystem(updateProviderFiles bool) (err error) {
|
||||
showInfo(fmt.Sprintf("UUID:%s", Settings.UUID))
|
||||
showInfo(fmt.Sprintf("Tuner (Plex / Emby):%d", Settings.Tuner))
|
||||
showInfo(fmt.Sprintf("EPG Source:%s", Settings.EpgSource))
|
||||
showInfo(fmt.Sprintf("Plex Channel Limit:%d", System.DVRLimit))
|
||||
showInfo(fmt.Sprintf("Plex Channel Limit:%d", System.PlexChannelLimit))
|
||||
showInfo(fmt.Sprintf("Unfiltered Chan. Limit:%d", System.UnfilteredChannelLimit))
|
||||
|
||||
// Providerdaten aktualisieren
|
||||
if len(Settings.Files.M3U) > 0 && Settings.FilesUpdate == true || updateProviderFiles == true {
|
||||
|
||||
298
src/data.go
298
src/data.go
@@ -11,19 +11,23 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"../src/internal/authentication"
|
||||
"xteve/src/internal/authentication"
|
||||
"xteve/src/internal/imgcache"
|
||||
)
|
||||
|
||||
// Einstellungen ändern (WebUI)
|
||||
func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err error) {
|
||||
func updateServerSettings(request RequestStruct) (settings SettingsStruct, err error) {
|
||||
|
||||
var oldSettings = jsonToMap(mapToJSON(Settings))
|
||||
var newSettings = jsonToMap(mapToJSON(request.Settings))
|
||||
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}'
|
||||
|
||||
for key, value := range newSettings {
|
||||
|
||||
if _, ok := oldSettings[key]; ok {
|
||||
@@ -33,14 +37,29 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, 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
|
||||
|
||||
case "update":
|
||||
// Leerzeichen aus den Werten entfernen und Formatierung der Uhrzeit überprüfen (0000 - 2359)
|
||||
var newUpdateTimes []string
|
||||
var newUpdateTimes = make([]string, 0)
|
||||
|
||||
for _, v := range value.([]interface{}) {
|
||||
for _, v := range value.([]any) {
|
||||
|
||||
v = strings.Replace(v.(string), " ", "", -1)
|
||||
|
||||
@@ -54,6 +73,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
}
|
||||
|
||||
if len(newUpdateTimes) == 0 {
|
||||
//newUpdateTimes = append(newUpdateTimes, "0000")
|
||||
}
|
||||
|
||||
value = newUpdateTimes
|
||||
|
||||
case "cache.images":
|
||||
@@ -62,6 +85,16 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, 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))
|
||||
@@ -94,26 +127,44 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
return
|
||||
}
|
||||
|
||||
case "ffmpeg.path", "vlc.path":
|
||||
var path = value.(string)
|
||||
if len(path) > 0 {
|
||||
|
||||
err = checkFile(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case "scheme.m3u", "scheme.xml":
|
||||
createXEPGFiles = true
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -138,6 +189,33 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
}
|
||||
|
||||
// Buffer Einstellungen überprüfen
|
||||
if len(Settings.FFmpegOptions) == 0 {
|
||||
Settings.FFmpegOptions = System.FFmpeg.DefaultOptions
|
||||
}
|
||||
|
||||
if len(Settings.VLCOptions) == 0 {
|
||||
Settings.VLCOptions = System.VLC.DefaultOptions
|
||||
}
|
||||
|
||||
switch Settings.Buffer {
|
||||
|
||||
case "ffmpeg":
|
||||
|
||||
if len(Settings.FFmpegPath) == 0 {
|
||||
err = errors.New(getErrMsg(2020))
|
||||
return
|
||||
}
|
||||
|
||||
case "vlc":
|
||||
|
||||
if len(Settings.VLCPath) == 0 {
|
||||
err = errors.New(getErrMsg(2021))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err = saveSettings(Settings)
|
||||
if err == nil {
|
||||
|
||||
@@ -156,25 +234,38 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
if cacheImages == true {
|
||||
|
||||
if Settings.EpgSource == "XEPG" {
|
||||
if Settings.EpgSource == "XEPG" && System.ImageCachingInProgress == 0 {
|
||||
|
||||
go func() {
|
||||
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
if Settings.CacheImages == true {
|
||||
switch Settings.CacheImages {
|
||||
|
||||
createXMLTVFile()
|
||||
cachingImages()
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
case false:
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
|
||||
} else {
|
||||
case true:
|
||||
go func() {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
|
||||
}
|
||||
System.ImageCachingInProgress = 1
|
||||
showInfo("Image Caching:Images are cached")
|
||||
|
||||
}()
|
||||
Data.Cache.Images.Image.Caching()
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
buildXEPG(false)
|
||||
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -189,6 +280,10 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err e
|
||||
|
||||
}
|
||||
|
||||
if triggerPlexGuideReload == true {
|
||||
queuePlexGuideRefresh("settings change")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
@@ -197,8 +292,8 @@ func updateServerSettings(request RequestStruct) (settings SettingsStrcut, 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
|
||||
|
||||
@@ -220,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 {
|
||||
@@ -229,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
|
||||
|
||||
}
|
||||
@@ -258,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)
|
||||
@@ -271,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
|
||||
@@ -304,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 {
|
||||
|
||||
@@ -334,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 {
|
||||
@@ -361,11 +456,12 @@ func deleteLocalProviderFiles(dataID, fileType string) {
|
||||
}
|
||||
|
||||
// Filtereinstellungen speichern (WebUI)
|
||||
func saveFilter(request RequestStruct) (settings SettingsStrcut, err error) {
|
||||
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
|
||||
|
||||
defaultFilter.Active = true
|
||||
defaultFilter.CaseSensitive = false
|
||||
@@ -389,23 +485,37 @@ func saveFilter(request RequestStruct) (settings SettingsStrcut, err error) {
|
||||
if dataID == -1 {
|
||||
|
||||
// Neuer Filter
|
||||
newFilter = true
|
||||
dataID = createNewID()
|
||||
filterMap[dataID] = jsonToMap(mapToJSON(defaultFilter))
|
||||
|
||||
}
|
||||
|
||||
// Filter aktualisieren / löschen
|
||||
for key, value := range data.(map[string]interface{}) {
|
||||
|
||||
var oldData = filterMap[dataID].(map[string]interface{})
|
||||
oldData[key] = value
|
||||
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]any)["filter"].(string); ok {
|
||||
|
||||
if len(filter) == 0 {
|
||||
|
||||
err = errors.New(getErrMsg(1014))
|
||||
if newFilter == true {
|
||||
delete(filterMap, dataID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if oldData, ok := filterMap[dataID].(map[string]any); ok {
|
||||
oldData[key] = value
|
||||
}
|
||||
|
||||
}
|
||||
@@ -434,6 +544,11 @@ func saveXEpgMapping(request RequestStruct) (err error) {
|
||||
|
||||
var tmp = Data.XEPG
|
||||
|
||||
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(mapToJSON(request.EpgMapping)), &tmp)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -446,8 +561,44 @@ func saveXEpgMapping(request RequestStruct) (err error) {
|
||||
|
||||
Data.XEPG.Channels = request.EpgMapping
|
||||
|
||||
cleanupXEPG()
|
||||
buildXEPG(true)
|
||||
if System.ScanInProgress == 0 {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
cleanupXEPG()
|
||||
System.ScanInProgress = 0
|
||||
buildXEPG(true)
|
||||
|
||||
} else {
|
||||
|
||||
// Wenn während des erstellen der Datanbank das Mapping erneut gespeichert wird, wird die Datenbank erst später erneut aktualisiert.
|
||||
go func() {
|
||||
|
||||
if System.BackgroundProcess == true {
|
||||
return
|
||||
}
|
||||
|
||||
System.BackgroundProcess = true
|
||||
|
||||
for {
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
if System.ScanInProgress == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.ScanInProgress = 1
|
||||
cleanupXEPG()
|
||||
System.ScanInProgress = 0
|
||||
buildXEPG(false)
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
System.BackgroundProcess = false
|
||||
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -456,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 {
|
||||
@@ -476,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
|
||||
}
|
||||
@@ -486,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
|
||||
}
|
||||
@@ -545,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
|
||||
@@ -612,6 +763,7 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
}
|
||||
|
||||
buildXEPG(false)
|
||||
System.ScanInProgress = 0
|
||||
|
||||
}
|
||||
|
||||
@@ -619,6 +771,10 @@ func saveWizard(request RequestStruct) (nextStep int, err error) {
|
||||
|
||||
}
|
||||
|
||||
if nextStep == 10 {
|
||||
Settings.WizardCompleted = true
|
||||
}
|
||||
|
||||
err = saveSettings(Settings)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -679,9 +835,9 @@ func buildDatabaseDVR() (err error) {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
|
||||
Data.Streams.All = make([]interface{}, 0)
|
||||
Data.Streams.Active = make([]interface{}, 0)
|
||||
Data.Streams.Inactive = make([]interface{}, 0)
|
||||
Data.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{}
|
||||
@@ -702,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)
|
||||
@@ -837,20 +993,20 @@ func buildDatabaseDVR() (err error) {
|
||||
sort.Strings(Data.Playlist.M3U.Groups.Text)
|
||||
sort.Strings(Data.Playlist.M3U.Groups.Value)
|
||||
|
||||
if len(Data.Streams.Active) == 0 && len(Data.Streams.All) <= System.DVRLimit && len(Settings.Filter) == 0 {
|
||||
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{}
|
||||
|
||||
}
|
||||
|
||||
if len(Data.Streams.Active) > System.DVRLimit {
|
||||
if len(Data.Streams.Active) > System.PlexChannelLimit {
|
||||
showWarning(2000)
|
||||
}
|
||||
|
||||
if len(Settings.Filter) == 0 && len(Data.Streams.All) > System.DVRLimit {
|
||||
if len(Settings.Filter) == 0 && len(Data.Streams.All) > System.UnfilteredChannelLimit {
|
||||
showWarning(2001)
|
||||
}
|
||||
|
||||
@@ -862,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
|
||||
}
|
||||
|
||||
@@ -869,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 {
|
||||
|
||||
@@ -897,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":
|
||||
@@ -910,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
|
||||
@@ -928,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":
|
||||
@@ -941,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)
|
||||
@@ -95,7 +95,7 @@ func getLineupStatus() (jsonContent []byte, err error) {
|
||||
lineupStatus.ScanInProgress = System.ScanInProgress
|
||||
lineupStatus.ScanPossible = 0
|
||||
lineupStatus.Source = "Cable"
|
||||
lineupStatus.SourceList = []string{"IPTV", "Cable"}
|
||||
lineupStatus.SourceList = []string{"Cable"}
|
||||
|
||||
jsonContent, err = json.MarshalIndent(lineupStatus, "", " ")
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
142
src/images.go
142
src/images.go
@@ -3,155 +3,15 @@ package src
|
||||
import (
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getCacheImageURL(imageURL string) (cacheImageURL string) {
|
||||
|
||||
if Settings.CacheImages == false {
|
||||
return imageURL
|
||||
}
|
||||
|
||||
imageURL = strings.Trim(imageURL, "\r\n")
|
||||
|
||||
p, err := url.Parse(imageURL)
|
||||
if err != nil {
|
||||
// URL konnte nicht geparst werden, die ursprüngliche image url wird zurückgegeben
|
||||
showInfo(fmt.Sprintf("Image Caching:Image URL: %s", imageURL))
|
||||
showWarning(4101)
|
||||
return imageURL
|
||||
}
|
||||
var urlMD5 = getMD5(imageURL)
|
||||
var fileExtension = filepath.Ext(p.Path)
|
||||
|
||||
if len(fileExtension) == 0 {
|
||||
// Keine Dateierweiterung vorhanden, die ursprüngliche image url wird zurückgegeben
|
||||
return imageURL
|
||||
}
|
||||
|
||||
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesFiles) == -1 {
|
||||
Data.Cache.ImagesFiles = append(Data.Cache.ImagesFiles, urlMD5+fileExtension)
|
||||
}
|
||||
|
||||
if System.ImageCachingInProgress == 1 {
|
||||
return imageURL
|
||||
}
|
||||
|
||||
if indexOfString(urlMD5+fileExtension, Data.Cache.ImagesCache) != -1 {
|
||||
|
||||
cacheImageURL = fmt.Sprintf("%s://%s/images/%s%s", System.ServerProtocol.WEB, System.Domain, urlMD5, fileExtension)
|
||||
|
||||
} else {
|
||||
|
||||
if strings.Contains(imageURL, System.Domain+"/images/") == false {
|
||||
|
||||
if indexOfString(imageURL, Data.Cache.ImagesURLS) == -1 {
|
||||
Data.Cache.ImagesURLS = append(Data.Cache.ImagesURLS, imageURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
cacheImageURL = imageURL
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func cachingImages() {
|
||||
|
||||
if Settings.CacheImages == false || System.ImageCachingInProgress == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
System.ImageCachingInProgress = 1
|
||||
|
||||
showInfo("Image Caching:Images are cached")
|
||||
|
||||
for _, imageURL := range Data.Cache.ImagesURLS {
|
||||
|
||||
if len(imageURL) > 0 {
|
||||
cacheImage(imageURL)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
// Bilder die nicht mehr verwendet werden, werden gelöscht
|
||||
files, err := ioutil.ReadDir(System.Folder.ImagesCache)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
if indexOfString(file.Name(), Data.Cache.ImagesFiles) == -1 {
|
||||
|
||||
var debug = fmt.Sprintf("Image Caching:Remove file: %s %s %d", System.Folder.ImagesCache+file.Name(), file.Name(), len(file.Name()))
|
||||
showDebug(debug, 1)
|
||||
err := os.RemoveAll(System.Folder.ImagesCache + file.Name())
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func cacheImage(imageURL string) {
|
||||
|
||||
var debug string
|
||||
var urlMD5 = getMD5(imageURL)
|
||||
var fileExtension = filepath.Ext(imageURL)
|
||||
|
||||
debug = fmt.Sprintf("Image Caching:File: %s Download: %s", urlMD5+fileExtension, imageURL)
|
||||
showDebug(debug, 1)
|
||||
|
||||
resp, err := http.Get(imageURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
|
||||
var filePath = System.Folder.ImagesCache + urlMD5 + fileExtension
|
||||
|
||||
// Datei speichern
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func uploadLogo(input, filename string) (logoURL string, err error) {
|
||||
|
||||
b64data := input[strings.IndexByte(input, ',')+1:]
|
||||
|
||||
// BAse64 in bytes umwandeln un speichern
|
||||
sDec, err := b64.StdEncoding.DecodeString(b64data)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -163,7 +23,7 @@ func uploadLogo(input, filename string) (logoURL string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.WEB, System.Domain, filename)
|
||||
logoURL = fmt.Sprintf("%s://%s/data_images/%s", System.ServerProtocol.XML, System.Domain, filename)
|
||||
|
||||
return
|
||||
|
||||
|
||||
132
src/info.go
132
src/info.go
@@ -1,99 +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: %t", Settings.Buffer))
|
||||
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
|
||||
}
|
||||
|
||||
178
src/internal/imgcache/cache.go
Normal file
178
src/internal/imgcache/cache.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Cache : Cache strcut
|
||||
type Cache struct {
|
||||
path string
|
||||
cacheURL string
|
||||
caching bool
|
||||
images map[string]string
|
||||
Queue []string
|
||||
Cache []string
|
||||
Image imageFunc
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type imageFunc struct {
|
||||
GetURL func(string) string
|
||||
Caching func()
|
||||
Remove func()
|
||||
}
|
||||
|
||||
// New : New cahce
|
||||
func New(path, chacheURL string, caching bool) (c *Cache, err error) {
|
||||
|
||||
c = &Cache{}
|
||||
|
||||
c.images = make(map[string]string)
|
||||
c.path = path
|
||||
c.cacheURL = chacheURL
|
||||
c.caching = caching
|
||||
c.Queue = []string{}
|
||||
c.Cache = []string{}
|
||||
|
||||
var queue []string
|
||||
|
||||
c.Image.GetURL = func(src string) (cacheURL string) {
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
src = strings.Trim(src, "\r\n")
|
||||
|
||||
if c.caching == false {
|
||||
return src
|
||||
}
|
||||
|
||||
u, err := url.Parse(src)
|
||||
if err != nil || len(filepath.Ext(u.Path)) == 0 {
|
||||
return src
|
||||
}
|
||||
|
||||
var filename = fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))
|
||||
if cacheURL, ok := c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))]; ok {
|
||||
return cacheURL
|
||||
}
|
||||
|
||||
if indexOfString(filename, c.Cache) == -1 {
|
||||
|
||||
if indexOfString(src, c.Queue) == -1 {
|
||||
c.Queue = append(c.Queue, src)
|
||||
}
|
||||
|
||||
} else {
|
||||
c.images[filename] = c.cacheURL + filename
|
||||
src = c.cacheURL + filename
|
||||
}
|
||||
|
||||
/*
|
||||
if _, err := os.Stat(c.path + filename); err != nil {
|
||||
//c.images[filename] = c.cacheURL + filename
|
||||
if indexOfString(src, c.Queue) == -1 {
|
||||
c.Queue = append(c.Queue, src)
|
||||
}
|
||||
} else {
|
||||
c.images[filename] = c.cacheURL + filename
|
||||
}
|
||||
*/
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
c.Image.Caching = func() {
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
var filename string
|
||||
|
||||
for _, src := range c.Queue {
|
||||
|
||||
resp, err := http.Get(src)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
continue
|
||||
}
|
||||
|
||||
filename = fmt.Sprintf("%s%s%s%s", c.path, string(os.PathSeparator), strToMD5(src), filepath.Ext(src))
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
u, err := url.Parse(src)
|
||||
if err == nil {
|
||||
c.images[fmt.Sprintf("%s%s", strToMD5(src), filepath.Ext(u.Path))] = c.cacheURL + filename
|
||||
}
|
||||
|
||||
queue = append(queue, src)
|
||||
|
||||
}
|
||||
|
||||
for _, q := range queue {
|
||||
c.Queue = removeStringFromSlice(q, c.Queue)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
c.Image.Remove = func() {
|
||||
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
files, err := ioutil.ReadDir(c.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
||||
switch c.caching {
|
||||
|
||||
case true:
|
||||
if _, ok := c.images[file.Name()]; !ok {
|
||||
os.RemoveAll(c.path + file.Name())
|
||||
}
|
||||
|
||||
case false:
|
||||
os.RemoveAll(c.path + file.Name())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(c.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
c.Cache = append(c.Cache, file.Name())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
34
src/internal/imgcache/tools.go
Normal file
34
src/internal/imgcache/tools.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func strToMD5(str string) string {
|
||||
md5Hasher := md5.New()
|
||||
md5Hasher.Write([]byte(str))
|
||||
return hex.EncodeToString(md5Hasher.Sum(nil))
|
||||
}
|
||||
|
||||
func indexOfString(str string, slice []string) int {
|
||||
|
||||
for i, v := range slice {
|
||||
if str == v {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func removeStringFromSlice(str string, slice []string) []string {
|
||||
|
||||
var i = indexOfString(str, slice)
|
||||
|
||||
if i != -1 {
|
||||
slice = append(slice[:i], slice[i+1:]...)
|
||||
}
|
||||
|
||||
return slice
|
||||
}
|
||||
@@ -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,7 +1,13 @@
|
||||
#EXTM3U url-tvg="http://example.com/file.xml" x-tvg-url="http://example.com/xteve.xml"
|
||||
#EXTINF:0 channelID="1" tvg-chno="1" tvg-name="Channel.1" tvg-id="tvg.id.1" tvg-logo="https://example/logo.png" group-title="Group 1",Channel 1
|
||||
#EXTINF:0 channelID="1" tvg-chno="1" tvg-name="Channel.1" tvg-id="tvg.id.1" tvg-logo="https://example/logo.png" group-title="Group 1", Channel 1
|
||||
http://example.com/stream/1
|
||||
|
||||
#EXTINF:0 channelID="2" tvg-chno="2" tvg-name="Channel.2" tvg-id="tvg.id.2" tvg-logo="https://example/logo.png" group-title="Group 2",Channel 2
|
||||
#123
|
||||
http://example.com/stream/2
|
||||
|
||||
#EXTINF:123, Sample artist - Sample title
|
||||
http://example.com/stream/3
|
||||
|
||||
#EXTINF:321,Example Artist - Example title
|
||||
http://example.com/stream/4
|
||||
|
||||
@@ -1,162 +1,186 @@
|
||||
package m3u
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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 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.Split(p, "=")
|
||||
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") {
|
||||
|
||||
stream["_uuid.key"] = key
|
||||
stream["_uuid.value"] = value
|
||||
//os.Exit(0)
|
||||
break
|
||||
if indexOfString(value, uuids) != -1 {
|
||||
log.Println(fmt.Sprintf("Channel: %s - %s = %s ", stream["name"], key, value))
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
uuids = append(uuids, value)
|
||||
|
||||
}
|
||||
stream["_uuid.key"] = key
|
||||
stream["_uuid.value"] = value
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//fmt.Println(content)
|
||||
}
|
||||
|
||||
if strings.Contains(content, "#EXTM3U") {
|
||||
return
|
||||
}
|
||||
|
||||
var channels = strings.Split(content, "#EXTINF")
|
||||
//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
|
||||
}
|
||||
|
||||
channels = append(channels[:0], channels[1:]...)
|
||||
if strings.Contains(content, "#EXTM3U") {
|
||||
|
||||
for _, channel := range channels {
|
||||
var channels = strings.Split(content, "#EXTINF")
|
||||
|
||||
var stream = parseMetaData(channel)
|
||||
channels = append(channels[:0], channels[1:]...)
|
||||
|
||||
if len(stream) > 0 && stream != nil {
|
||||
allChannels = append(allChannels, stream)
|
||||
}
|
||||
for _, channel := range channels {
|
||||
|
||||
}
|
||||
var stream = parseMetaData(channel)
|
||||
|
||||
} else {
|
||||
if len(stream) > 0 && stream != nil {
|
||||
allChannels = append(allChannels, stream)
|
||||
}
|
||||
|
||||
err = errors.New("No valid m3u file")
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
|
||||
return
|
||||
err = errors.New("Invalid M3U file, an extended M3U file is required.")
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func indexOfString(element string, data []string) int {
|
||||
|
||||
for k, v := range data {
|
||||
if element == v {
|
||||
return k
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
14
src/m3u.go
14
src/m3u.go
@@ -9,11 +9,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
m3u "../src/internal/m3u-parser"
|
||||
m3u "xteve/src/internal/m3u-parser"
|
||||
)
|
||||
|
||||
// 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)
|
||||
@@ -43,6 +43,10 @@ func filterThisStream(s interface{}) (status bool) {
|
||||
|
||||
for _, filter := range Data.Filter {
|
||||
|
||||
if filter.Rule == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var group, name, search string
|
||||
var exclude, include string
|
||||
var match = false
|
||||
@@ -177,6 +181,7 @@ func checkConditions(streamValues, conditions, coType string) (status bool) {
|
||||
// xTeVe M3U Datei erstellen
|
||||
func buildM3U(groups []string) (m3u string, err error) {
|
||||
|
||||
var imgc = Data.Cache.Images
|
||||
var m3uChannels = make(map[float64]XEPGChannelStruct)
|
||||
var channelNumbers []float64
|
||||
|
||||
@@ -219,7 +224,8 @@ func buildM3U(groups []string) (m3u string, err error) {
|
||||
for _, channelNumber := range channelNumbers {
|
||||
|
||||
var channel = m3uChannels[channelNumber]
|
||||
var parameter = fmt.Sprintf(`#EXTINF:0 channelID="%s" tvg-chno="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s" group-title="%s",%s`+"\n", channel.XEPG, channel.XChannelID, channel.XName, channel.XChannelID, getCacheImageURL(channel.TvgLogo), channel.XGroupTitle, channel.XName)
|
||||
|
||||
var parameter = fmt.Sprintf(`#EXTINF:0 channelID="%s" tvg-chno="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s" group-title="%s",%s`+"\n", channel.XEPG, channel.XChannelID, channel.XName, channel.XChannelID, imgc.Image.GetURL(channel.TvgLogo), channel.XGroupTitle, channel.XName)
|
||||
var stream, err = createStreamingURL("M3U", channel.FileM3UID, channel.XChannelID, channel.XName, channel.URL)
|
||||
if err == nil {
|
||||
m3u = m3u + parameter + stream + "\n"
|
||||
|
||||
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
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
m3u "../src/internal/m3u-parser"
|
||||
m3u "xteve/src/internal/m3u-parser"
|
||||
)
|
||||
|
||||
// fileType: Welcher Dateityp soll aktualisiert werden (m3u, hdhr, xml) | fileID: Update einer bestimmten Datei (Provider ID)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func showInfo(str string) {
|
||||
return
|
||||
}
|
||||
|
||||
var max = 22
|
||||
var max = 23
|
||||
var msg = strings.SplitN(str, ":", 2)
|
||||
var length = len(msg[0])
|
||||
var space string
|
||||
@@ -48,7 +48,7 @@ func showDebug(str string, level int) {
|
||||
return
|
||||
}
|
||||
|
||||
var max = 22
|
||||
var max = 23
|
||||
var msg = strings.SplitN(str, ":", 2)
|
||||
var length = len(msg[0])
|
||||
var space string
|
||||
@@ -78,7 +78,7 @@ func showDebug(str string, level int) {
|
||||
|
||||
func showHighlight(str string) {
|
||||
|
||||
var max = 22
|
||||
var max = 23
|
||||
var msg = strings.SplitN(str, ":", 2)
|
||||
var length = len(msg[0])
|
||||
var space string
|
||||
@@ -232,7 +232,7 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
case 1004:
|
||||
errMsg = fmt.Sprintf("File not found")
|
||||
case 1005:
|
||||
errMsg = fmt.Sprintf("Invalide m3u")
|
||||
errMsg = fmt.Sprintf("Invalid M3U file, an extended M3U file is required.")
|
||||
case 1006:
|
||||
errMsg = fmt.Sprintf("No playlist!")
|
||||
case 1007:
|
||||
@@ -245,6 +245,8 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
errMsg = fmt.Sprintf("Invalid formatting of the time")
|
||||
case 1013:
|
||||
errMsg = fmt.Sprintf("Invalid settings file (settings.json), file must be at least version %s", System.Compatibility)
|
||||
case 1014:
|
||||
errMsg = fmt.Sprintf("Invalid filter rule")
|
||||
|
||||
case 1020:
|
||||
errMsg = fmt.Sprintf("Data could not be saved, invalid keyword")
|
||||
@@ -252,12 +254,13 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
// Datenbank Update
|
||||
case 1030:
|
||||
errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings)
|
||||
case 1031:
|
||||
errMsg = fmt.Sprintf("Database error. The database version of your settings is not compatible with this version.")
|
||||
|
||||
// M3U Parser
|
||||
case 1050:
|
||||
errMsg = fmt.Sprintf("Invalid duration specification in the M3U8 playlist.")
|
||||
|
||||
// M3U Parser
|
||||
case 1060:
|
||||
errMsg = fmt.Sprintf("Invalid characters found in the tvg parameters, streams with invalid parameters were skipped.")
|
||||
|
||||
@@ -266,6 +269,8 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
errMsg = fmt.Sprintf("Folder could not be created.")
|
||||
case 1071:
|
||||
errMsg = fmt.Sprintf("File could not be created")
|
||||
case 1072:
|
||||
errMsg = fmt.Sprintf("File not found")
|
||||
|
||||
// Backup
|
||||
case 1090:
|
||||
@@ -290,12 +295,14 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist")
|
||||
case 1203:
|
||||
errMsg = fmt.Sprintf("Steaming URL could not be found in any playlist")
|
||||
case 1204:
|
||||
errMsg = fmt.Sprintf("Streaming was stopped by third party transcoder (FFmpeg / VLC)")
|
||||
|
||||
// Warnings
|
||||
case 2000:
|
||||
errMsg = fmt.Sprintf("Plex can not handle more than %d streams. If you do not use Plex, you can ignore this warning.", System.DVRLimit)
|
||||
errMsg = fmt.Sprintf("Plex can not handle more than %d streams. If you do not use Plex, you can ignore this warning.", System.PlexChannelLimit)
|
||||
case 2001:
|
||||
errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.DVRLimit)
|
||||
errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.UnfilteredChannelLimit)
|
||||
case 2002:
|
||||
errMsg = fmt.Sprintf("PMS can not play m3u8 streams")
|
||||
case 2003:
|
||||
@@ -306,13 +313,17 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
errMsg = fmt.Sprintf("There are no channels mapped, use the mapping menu to assign EPG data to the channels.")
|
||||
case 2010:
|
||||
errMsg = fmt.Sprintf("No valid streaming URL")
|
||||
case 2020:
|
||||
errMsg = fmt.Sprintf("FFmpeg binary was not found. Check the FFmpeg binary path in the xTeVe settings.")
|
||||
case 2021:
|
||||
errMsg = fmt.Sprintf("VLC binary was not found. Check the VLC path binary in the xTeVe settings.")
|
||||
|
||||
case 2099:
|
||||
errMsg = fmt.Sprintf("Updates have been disabled by the developer")
|
||||
|
||||
// Tuner
|
||||
case 2105:
|
||||
errMsg = fmt.Sprintf("The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again.")
|
||||
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")
|
||||
|
||||
@@ -345,6 +356,8 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
errMsg = fmt.Sprintf("This error message comes from the provider")
|
||||
case 4005:
|
||||
errMsg = fmt.Sprintf("Temporary buffer files could not be deleted")
|
||||
case 4006:
|
||||
errMsg = fmt.Sprintf("Server connection timeout")
|
||||
|
||||
// Buffer (M3U8)
|
||||
case 4050:
|
||||
@@ -368,7 +381,7 @@ func getErrMsg(errCode int) (errMsg string) {
|
||||
case 6002:
|
||||
errMsg = fmt.Sprintf("Update failed")
|
||||
case 6003:
|
||||
errMsg = fmt.Sprintf("Server not available")
|
||||
errMsg = fmt.Sprintf("Update server not available")
|
||||
case 6004:
|
||||
errMsg = fmt.Sprintf("xTeVe update available")
|
||||
|
||||
|
||||
@@ -108,3 +108,147 @@ type BandwidthCalculation struct {
|
||||
Stop time.Time
|
||||
TimeDiff float64
|
||||
}
|
||||
|
||||
/*
|
||||
var args = "-hide_banner -loglevel panic -re -i " + url + " -codec copy -f mpegts pipe:1"
|
||||
//var args = "-re -i " + url + " -codec copy -f mpegts pipe:1"
|
||||
cmd := exec.Command("/usr/local/bin/ffmpeg", strings.Split(args, " ")...)
|
||||
|
||||
//run := exec.Command("/usr/local/bin/ffmpeg", "-hide_banner", "-loglevel", "panic", "-re", "-i", url, "-codec", "copy", "-f", "mpegts", "pipe:1")
|
||||
//run := exec.Command("/usr/local/bin/ffmpeg", "-re", "-i", url, "-codec", "copy", "-f", "mpegts", "pipe:1")
|
||||
|
||||
stderr, _ := cmd.StderrPipe()
|
||||
cmd.Start()
|
||||
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
m := scanner.Text()
|
||||
fmt.Println(m)
|
||||
}
|
||||
cmd.Wait()
|
||||
|
||||
os.Exit(0)
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
ffmpegOut, _ := run.StderrPipe()
|
||||
//run.Start()
|
||||
|
||||
scanner = bufio.NewScanner(ffmpegOut)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
m := scanner.Text()
|
||||
fmt.Println(m)
|
||||
}
|
||||
|
||||
ffmpegOut, err = run.StdoutPipe()
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, stderrErr := run.StderrPipe()
|
||||
if stderrErr != nil {
|
||||
fmt.Println(stderrErr)
|
||||
}
|
||||
|
||||
_ = stderr
|
||||
|
||||
if startErr := run.Start(); startErr != nil {
|
||||
fmt.Println(startErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
n, err := ffmpegOut.Read(buffer)
|
||||
_ = n
|
||||
_ = stream
|
||||
_ = fileSize
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
|
||||
ShowError(err, 0)
|
||||
addErrorToStream(err)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
defer bufferFile.Close()
|
||||
|
||||
scanner = bufio.NewScanner(ffmpegOut)
|
||||
|
||||
for scanner.Scan() {
|
||||
//fmt.Printf("%s\n", scanner.Text())
|
||||
//fmt.Println(scanner)
|
||||
thisLine := scanner.Text()
|
||||
line := make([]byte, len(thisLine))
|
||||
|
||||
buffer = append(buffer, line...)
|
||||
|
||||
fmt.Println(len(buffer))
|
||||
|
||||
if len(buffer) > tmpFileSize {
|
||||
|
||||
if _, err := bufferFile.Write(buffer[:]); err != nil {
|
||||
|
||||
ShowError(err, 0)
|
||||
addErrorToStream(err)
|
||||
run.Process.Kill()
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
buffer = make([]byte, 1024*Settings.BufferSize*2)
|
||||
|
||||
debug = fmt.Sprintf("Buffer Status:Done (%s)", tmpFile)
|
||||
showDebug(debug, 2)
|
||||
|
||||
bufferFile.Close()
|
||||
|
||||
stream.Status = true
|
||||
playlist.Streams[streamID] = stream
|
||||
BufferInformation.Store(playlistID, playlist)
|
||||
|
||||
tmpSegment++
|
||||
|
||||
tmpFile = fmt.Sprintf("%s%d.ts", tmpFolder, tmpSegment)
|
||||
|
||||
if clientConnection(stream) == false {
|
||||
|
||||
bufferFile.Close()
|
||||
run.Process.Kill()
|
||||
|
||||
err = os.RemoveAll(stream.Folder)
|
||||
if err != nil {
|
||||
ShowError(err, 4005)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
bufferFile, err = os.Create(tmpFile)
|
||||
if err != nil {
|
||||
addErrorToStream(err)
|
||||
run.Process.Kill()
|
||||
return
|
||||
}
|
||||
|
||||
fileSize = 0
|
||||
|
||||
if n == 0 {
|
||||
bufferFile.Close()
|
||||
run.Process.Kill()
|
||||
break
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package src
|
||||
|
||||
import "xteve/src/internal/imgcache"
|
||||
|
||||
// SystemStruct : Beinhaltet alle Systeminformationen
|
||||
type SystemStruct struct {
|
||||
Addresses struct {
|
||||
@@ -8,18 +10,30 @@ type SystemStruct struct {
|
||||
XML string
|
||||
}
|
||||
|
||||
APIVersion string
|
||||
AppName string
|
||||
ARCH string
|
||||
Branch string
|
||||
Build string
|
||||
Compatibility string
|
||||
ConfigurationWizard bool
|
||||
DBVersion string
|
||||
Dev bool
|
||||
DeviceID string
|
||||
Domain string
|
||||
DVRLimit int
|
||||
APIVersion string
|
||||
AppName string
|
||||
ARCH string
|
||||
BackgroundProcess bool
|
||||
Branch string
|
||||
Build string
|
||||
Compatibility string
|
||||
ConfigurationWizard bool
|
||||
DBVersion string
|
||||
Dev bool
|
||||
DeviceID string
|
||||
Domain string
|
||||
PlexChannelLimit int
|
||||
UnfilteredChannelLimit int
|
||||
|
||||
FFmpeg struct {
|
||||
DefaultOptions string
|
||||
Path string
|
||||
}
|
||||
|
||||
VLC struct {
|
||||
DefaultOptions string
|
||||
Path string
|
||||
}
|
||||
|
||||
File struct {
|
||||
Authentication string
|
||||
@@ -31,6 +45,10 @@ type SystemStruct struct {
|
||||
XML string
|
||||
}
|
||||
|
||||
Compressed struct {
|
||||
GZxml string
|
||||
}
|
||||
|
||||
Flag struct {
|
||||
Branch string
|
||||
Debug int
|
||||
@@ -84,6 +102,7 @@ type SystemStruct struct {
|
||||
}
|
||||
|
||||
URLBase string
|
||||
UDPxy string
|
||||
Version string
|
||||
WEB struct {
|
||||
Menu []string
|
||||
@@ -99,6 +118,7 @@ type GitStruct struct {
|
||||
// DataStruct : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
|
||||
type DataStruct struct {
|
||||
Cache struct {
|
||||
Images *imgcache.Cache
|
||||
ImagesCache []string
|
||||
ImagesFiles []string
|
||||
ImagesURLS []string
|
||||
@@ -129,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
|
||||
}
|
||||
}
|
||||
@@ -176,6 +196,7 @@ type XEPGChannelStruct struct {
|
||||
XName string `json:"x-name,required"`
|
||||
XUpdateChannelIcon bool `json:"x-update-channel-icon,required"`
|
||||
XUpdateChannelName bool `json:"x-update-channel-name,required"`
|
||||
XDescription string `json:"x-description,required"`
|
||||
}
|
||||
|
||||
// M3UChannelStructXEPG : M3U Struktur für XEPG
|
||||
@@ -230,9 +251,12 @@ type Notification struct {
|
||||
Type string `json:"type,required"`
|
||||
}
|
||||
|
||||
// SettingsStrcut : Inhalt der settings.json
|
||||
type SettingsStrcut 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"`
|
||||
@@ -241,38 +265,45 @@ type SettingsStrcut struct {
|
||||
BackupKeep int `json:"backup.keep"`
|
||||
BackupPath string `json:"backup.path"`
|
||||
Branch string `json:"git.branch,omitempty"`
|
||||
Buffer bool `json:"buffer"`
|
||||
Buffer string `json:"buffer"`
|
||||
BufferSize int `json:"buffer.size.kb"`
|
||||
BufferTimeout float64 `json:"buffer.timeout"`
|
||||
CacheImages bool `json:"cache.images"`
|
||||
EpgSource string `json:"epgSource"`
|
||||
FFmpegOptions string `json:"ffmpeg.options"`
|
||||
FFmpegPath string `json:"ffmpeg.path"`
|
||||
VLCOptions string `json:"vlc.options"`
|
||||
VLCPath string `json:"vlc.path"`
|
||||
FileM3U []string `json:"file,omitempty"` // Beim Wizard wird die M3U in ein Slice gespeichert
|
||||
FileXMLTV []string `json:"xmltv,omitempty"` // Altes Speichersystem der Provider XML Datei Slice (Wird für die Umwandlung auf das neue benötigt)
|
||||
|
||||
Files struct {
|
||||
HDHR map[string]interface{} `json:"hdhr"`
|
||||
M3U map[string]interface{} `json:"m3u"`
|
||||
XMLTV map[string]interface{} `json:"xmltv"`
|
||||
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"`
|
||||
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"`
|
||||
@@ -25,32 +28,40 @@ type RequestStruct struct {
|
||||
AuthenticationXML *bool `json:"authentication.xml,omitempty"`
|
||||
BackupKeep *int `json:"backup.keep,omitempty"`
|
||||
BackupPath *string `json:"backup.path,omitempty"`
|
||||
Buffer *bool `json:"buffer,omitempty"`
|
||||
BufferSize *int `json:"buffer.size.kb, omitempty"`
|
||||
Buffer *string `json:"buffer,omitempty"`
|
||||
BufferSize *int `json:"buffer.size.kb,omitempty"`
|
||||
BufferTimeout *float64 `json:"buffer.timeout,omitempty"`
|
||||
CacheImages *bool `json:"cache.images,omitempty"`
|
||||
EpgSource *string `json:"epgSource,omitempty"`
|
||||
FFmpegOptions *string `json:"ffmpeg.options,omitempty"`
|
||||
FFmpegPath *string `json:"ffmpeg.path,omitempty"`
|
||||
VLCOptions *string `json:"vlc.options,omitempty"`
|
||||
VLCPath *string `json:"vlc.path,omitempty"`
|
||||
FilesUpdate *bool `json:"files.update,omitempty"`
|
||||
TempPath *string `json:"temp.path,omitempty"`
|
||||
Tuner *int `json:"tuner,omitempty"`
|
||||
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"`
|
||||
} `json:"settings,omitempty"`
|
||||
SchemeM3U *string `json:"scheme.m3u,omitempty"`
|
||||
SchemeXML *string `json:"scheme.xml,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 {
|
||||
@@ -58,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)
|
||||
@@ -77,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 {
|
||||
@@ -95,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 SettingsStrcut `json:"settings,required"`
|
||||
Status bool `json:"status,required"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Users map[string]interface{} `json:"users,omitempty"`
|
||||
Wizard int `json:"wizard,omitempty"`
|
||||
XEPG map[string]interface{} `json:"xepg,required"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -42,12 +42,16 @@ type Program struct {
|
||||
Country []*Country `xml:"country"`
|
||||
EpisodeNum []*EpisodeNum `xml:"episode-num"`
|
||||
Poster []Poster `xml:"icon"`
|
||||
Credits Credits `xml:"credits,omitempty"` //`xml:",innerxml,omitempty"`
|
||||
Rating []Rating `xml:"rating"`
|
||||
StarRating []StarRating `xml:"star-rating"`
|
||||
Language []*Language `xml:"language"`
|
||||
Video Video `xml:"video"`
|
||||
Date string `xml:"date"`
|
||||
PreviouslyShown *PreviouslyShown `xml:"previously-shown"`
|
||||
New *New `xml:"new"`
|
||||
Live *Live `xml:"live"`
|
||||
Premiere *Live `xml:"premiere"`
|
||||
}
|
||||
|
||||
// Title : Programmtitel
|
||||
@@ -74,6 +78,19 @@ type Category struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Rating : Bewertung
|
||||
type Rating struct {
|
||||
System string `xml:"system,attr"`
|
||||
Value string `xml:"value"`
|
||||
Icon []Icon `xml:"icon"`
|
||||
}
|
||||
|
||||
// StarRating : Bewertung / Kritiken
|
||||
type StarRating struct {
|
||||
Value string `xml:"value"`
|
||||
System string `xml:"system,attr"`
|
||||
}
|
||||
|
||||
// Language : Sprachen
|
||||
type Language struct {
|
||||
Value string `xml:",chardata"`
|
||||
@@ -99,6 +116,41 @@ type Poster struct {
|
||||
Width string `xml:"width,attr"`
|
||||
}
|
||||
|
||||
// Credits : Credits
|
||||
type Credits struct {
|
||||
Director []Director `xml:"director,omitempty"`
|
||||
Actor []Actor `xml:"actor,omitempty"`
|
||||
Writer []Writer `xml:"writer,omitempty"`
|
||||
Presenter []Presenter `xml:"presenter,omitempty"`
|
||||
Producer []Producer `xml:"producer,omitempty"`
|
||||
}
|
||||
|
||||
// Director : Director
|
||||
type Director struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Actor : Actor
|
||||
type Actor struct {
|
||||
Value string `xml:",chardata"`
|
||||
Role string `xml:"role,attr,omitempty"`
|
||||
}
|
||||
|
||||
// Writer : Writer
|
||||
type Writer struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Presenter : Presenter
|
||||
type Presenter struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Producer : Producer
|
||||
type Producer struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// Video : Video Metadaten
|
||||
type Video struct {
|
||||
Aspect string `xml:"aspect,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
|
||||
}
|
||||
@@ -90,22 +90,35 @@ func createSystemFiles() (err error) {
|
||||
}
|
||||
|
||||
// Einstellungen laden und default Werte setzen (xTeVe)
|
||||
func loadSettings() (settings SettingsStrcut, err error) {
|
||||
func loadSettings() (settings SettingsStruct, err error) {
|
||||
|
||||
settingsMap, err := loadJSONFileToMap(System.File.Settings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deafult Werte setzten
|
||||
var defaults = make(map[string]interface{})
|
||||
var dataMap = make(map[string]interface{})
|
||||
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
|
||||
@@ -113,29 +126,39 @@ func loadSettings() (settings SettingsStrcut, err error) {
|
||||
defaults["authentication.xml"] = false
|
||||
defaults["backup.keep"] = 10
|
||||
defaults["backup.path"] = System.Folder.Backup
|
||||
defaults["buffer"] = false
|
||||
defaults["buffer"] = "-"
|
||||
defaults["buffer.size.kb"] = 1024
|
||||
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 {
|
||||
@@ -159,15 +182,37 @@ func loadSettings() (settings SettingsStrcut, err error) {
|
||||
showInfo(fmt.Sprintf("Git Branch:Switching Git Branch to -> %s", settings.Branch))
|
||||
}
|
||||
|
||||
if len(settings.FFmpegPath) == 0 {
|
||||
containerFFmpegPath := "/usr/local/bin/ffmpeg"
|
||||
if len(os.Getenv("XTEVE_CONFIG")) > 0 && checkFile(containerFFmpegPath) == nil {
|
||||
settings.FFmpegPath = containerFFmpegPath
|
||||
} else {
|
||||
settings.FFmpegPath = searchFileInOS("ffmpeg")
|
||||
}
|
||||
}
|
||||
|
||||
if len(settings.VLCPath) == 0 {
|
||||
settings.VLCPath = searchFileInOS("cvlc")
|
||||
}
|
||||
|
||||
settings.Version = System.DBVersion
|
||||
|
||||
err = saveSettings(settings)
|
||||
|
||||
// Warung wenn FFmpeg nicht gefunden wurde
|
||||
if len(Settings.FFmpegPath) == 0 && Settings.Buffer == "ffmpeg" {
|
||||
showWarning(2020)
|
||||
}
|
||||
|
||||
if len(Settings.VLCPath) == 0 && Settings.Buffer == "vlc" {
|
||||
showWarning(2021)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Einstellungen speichern (xTeVe)
|
||||
func saveSettings(settings SettingsStrcut) (err error) {
|
||||
func saveSettings(settings SettingsStruct) (err error) {
|
||||
|
||||
if settings.BackupKeep == 0 {
|
||||
settings.BackupKeep = 10
|
||||
@@ -181,8 +226,36 @@ func saveSettings(settings SettingsStrcut) (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
|
||||
|
||||
@@ -10,8 +10,11 @@ import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
@@ -42,13 +45,25 @@ func checkFolder(path string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prüft ob die datei im Dateisystem existiert
|
||||
// Prüft ob die Datei im Dateisystem existiert
|
||||
func checkFile(filename string) (err error) {
|
||||
|
||||
var file = getPlatformFile(filename)
|
||||
|
||||
if _, err = os.Stat(file); os.IsNotExist(err) {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
err = fmt.Errorf("%s: %s", file, getErrMsg(1072))
|
||||
case mode.IsRegular():
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
@@ -77,6 +92,7 @@ func GetUserHomeDirectory() (userHomeDirectory string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prüft Dateiberechtigung
|
||||
func checkFilePermission(dir string) (err error) {
|
||||
|
||||
var filename = dir + "permission.test"
|
||||
@@ -115,7 +131,34 @@ func removeOldSystemData() {
|
||||
os.RemoveAll(System.Folder.Temp)
|
||||
}
|
||||
|
||||
//
|
||||
// Sucht eine Datei im OS
|
||||
func searchFileInOS(file string) (path string) {
|
||||
|
||||
switch runtime.GOOS {
|
||||
|
||||
case "linux", "darwin", "freebsd":
|
||||
var args = file
|
||||
var cmd = exec.Command("which", strings.Split(args, " ")...)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
|
||||
var slice = strings.Split(strings.Replace(string(out), "\r\n", "\n", -1), "\n")
|
||||
|
||||
if len(slice) > 0 {
|
||||
path = strings.Trim(slice[0], "\r\n")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func removeChildItems(dir string) error {
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*"))
|
||||
@@ -136,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 {
|
||||
@@ -146,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, "", " ")
|
||||
@@ -186,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()
|
||||
@@ -276,6 +319,22 @@ func resolveHostIP() (err error) {
|
||||
|
||||
}
|
||||
|
||||
if len(System.IPAddress) == 0 {
|
||||
|
||||
switch len(System.IPAddressesV4) {
|
||||
|
||||
case 0:
|
||||
if len(System.IPAddressesV6) > 0 {
|
||||
System.IPAddress = System.IPAddressesV6[0]
|
||||
}
|
||||
|
||||
default:
|
||||
System.IPAddress = System.IPAddressesV4[0]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
System.Hostname, err = os.Hostname()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -300,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))
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
up2date "../src/internal/up2date/client"
|
||||
up2date "xteve/src/internal/up2date/client"
|
||||
|
||||
"reflect"
|
||||
)
|
||||
@@ -41,19 +41,19 @@ func BinaryUpdate() (err error) {
|
||||
|
||||
resp, err := http.Get(gitInfo)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
return err
|
||||
ShowError(err, 6003)
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
@@ -169,6 +169,13 @@ checkVersion:
|
||||
|
||||
if settingsVersion, ok := settingsMap["version"].(string); ok {
|
||||
|
||||
if settingsVersion > System.DBVersion {
|
||||
showInfo("Settings DB Version:" + settingsVersion)
|
||||
showInfo("System DB Version:" + System.DBVersion)
|
||||
err = errors.New(getErrMsg(1031))
|
||||
return
|
||||
}
|
||||
|
||||
// Letzte Kompatible Version (1.4.4)
|
||||
if settingsVersion < System.Compatibility {
|
||||
err = errors.New(getErrMsg(1013))
|
||||
@@ -185,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
|
||||
|
||||
@@ -204,10 +211,37 @@ checkVersion:
|
||||
}
|
||||
|
||||
case "2.0.0":
|
||||
|
||||
if oldBuffer, ok := settingsMap["buffer"].(bool); ok {
|
||||
|
||||
var newBuffer string
|
||||
switch oldBuffer {
|
||||
case true:
|
||||
newBuffer = "xteve"
|
||||
case false:
|
||||
newBuffer = "-"
|
||||
}
|
||||
|
||||
settingsMap["buffer"] = newBuffer
|
||||
|
||||
settingsMap["version"] = "2.1.0"
|
||||
|
||||
err = saveMapToJSONFile(System.File.Settings, settingsMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
goto checkVersion
|
||||
|
||||
} else {
|
||||
err = errors.New(getErrMsg(1030))
|
||||
return
|
||||
}
|
||||
|
||||
case "2.1.0":
|
||||
// Falls es in einem späteren Update Änderungen an der Datenbank gibt, geht es hier weiter
|
||||
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -218,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)
|
||||
@@ -251,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
@@ -10,11 +10,19 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"../src/internal/authentication"
|
||||
"xteve/src/internal/authentication"
|
||||
|
||||
"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) {
|
||||
|
||||
@@ -30,6 +38,7 @@ func StartWebserver() (err error) {
|
||||
http.HandleFunc("/api/", API)
|
||||
http.HandleFunc("/images/", Images)
|
||||
http.HandleFunc("/data_images/", DataImages)
|
||||
|
||||
//http.HandleFunc("/auto/", Auto)
|
||||
|
||||
showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port)
|
||||
@@ -129,20 +138,37 @@ func Stream(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Index(streamInfo.URL, "rtsp://") != -1 || strings.Index(streamInfo.URL, "rtp://") != -1 {
|
||||
err = errors.New("RTSP and RTP streams are not supported")
|
||||
ShowError(err, 2004)
|
||||
|
||||
showInfo("Streaming URL:" + streamInfo.URL)
|
||||
http.Redirect(w, r, streamInfo.URL, 302)
|
||||
|
||||
showInfo("Streaming Info:URL was passed to the client")
|
||||
return
|
||||
// If an UDPxy host is set, and the stream URL is multicast (i.e. starts with 'udp://@'),
|
||||
// then streamInfo.URL needs to be rewritten to point to UDPxy.
|
||||
if Settings.UDPxy != "" && strings.HasPrefix(streamInfo.URL, "udp://@") {
|
||||
streamInfo.URL = fmt.Sprintf("http://%s/udp/%s/", Settings.UDPxy, strings.TrimPrefix(streamInfo.URL, "udp://@"))
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("Buffer:%t", Settings.Buffer))
|
||||
switch Settings.Buffer {
|
||||
|
||||
if Settings.Buffer == true {
|
||||
case "-":
|
||||
showInfo(fmt.Sprintf("Buffer:false [%s]", Settings.Buffer))
|
||||
|
||||
case "xteve":
|
||||
if strings.Index(streamInfo.URL, "rtsp://") != -1 || strings.Index(streamInfo.URL, "rtp://") != -1 {
|
||||
err = errors.New("RTSP and RTP streams are not supported")
|
||||
ShowError(err, 2004)
|
||||
|
||||
showInfo("Streaming URL:" + streamInfo.URL)
|
||||
http.Redirect(w, r, streamInfo.URL, 302)
|
||||
|
||||
showInfo("Streaming Info:URL was passed to the client")
|
||||
return
|
||||
}
|
||||
|
||||
showInfo(fmt.Sprintf("Buffer:true [%s]", Settings.Buffer))
|
||||
|
||||
default:
|
||||
showInfo(fmt.Sprintf("Buffer:true [%s]", Settings.Buffer))
|
||||
|
||||
}
|
||||
|
||||
if Settings.Buffer != "-" {
|
||||
showInfo(fmt.Sprintf("Buffer Size:%d KB", Settings.BufferSize))
|
||||
}
|
||||
|
||||
@@ -152,16 +178,16 @@ func Stream(w http.ResponseWriter, r *http.Request) {
|
||||
// Prüfen ob der Buffer verwendet werden soll
|
||||
switch Settings.Buffer {
|
||||
|
||||
case true:
|
||||
bufferingStream(streamInfo.PlaylistID, streamInfo.URL, streamInfo.Name, w, r)
|
||||
|
||||
case false:
|
||||
case "-":
|
||||
showInfo("Streaming URL:" + streamInfo.URL)
|
||||
http.Redirect(w, r, streamInfo.URL, 302)
|
||||
|
||||
showInfo("Streaming Info:URL was passed to the client.")
|
||||
showInfo("Streaming Info:xTeVe is no longer involved, the client connects directly to the streaming server.")
|
||||
|
||||
default:
|
||||
bufferingStream(streamInfo.PlaylistID, streamInfo.URL, streamInfo.Name, w, r)
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
@@ -195,7 +221,7 @@ func Auto(w http.ResponseWriter, r *http.Request) {
|
||||
// xTeVe : Web Server /xmltv/ und /m3u/
|
||||
func xTeVe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var requestType, groupTitle, file, content string
|
||||
var requestType, groupTitle, file, content, contentType string
|
||||
var err error
|
||||
var path = strings.TrimPrefix(r.URL.Path, "/")
|
||||
var groups = []string{}
|
||||
@@ -248,6 +274,13 @@ func xTeVe(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
contentType = http.DetectContentType([]byte(content))
|
||||
if strings.Contains(strings.ToLower(contentType), "xml") {
|
||||
contentType = "application/xml; charset=utf-8"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
if err == nil {
|
||||
w.Write([]byte(content))
|
||||
}
|
||||
@@ -304,15 +337,18 @@ func WS(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var newToken string
|
||||
|
||||
if r.Header.Get("Origin") != "http://"+r.Host {
|
||||
httpStatusError(w, r, 403)
|
||||
return
|
||||
}
|
||||
/*
|
||||
if r.Header.Get("Origin") != "http://"+r.Host {
|
||||
httpStatusError(w, r, 403)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
setGlobalDomain(r.Host)
|
||||
@@ -530,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))
|
||||
@@ -563,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)
|
||||
@@ -571,6 +607,8 @@ func Web(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var language LanguageUI
|
||||
|
||||
setGlobalDomain(r.Host)
|
||||
|
||||
if System.Dev == true {
|
||||
|
||||
lang, err = loadJSONFileToMap(fmt.Sprintf("html/lang/%s.json", Settings.Language))
|
||||
@@ -599,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
|
||||
}
|
||||
|
||||
@@ -998,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 {
|
||||
|
||||
@@ -1007,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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
494
src/xepg.go
494
src/xepg.go
@@ -8,10 +8,16 @@ import (
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"xteve/src/internal/imgcache"
|
||||
)
|
||||
|
||||
// Provider XMLTV Datei überprüfen
|
||||
@@ -42,6 +48,13 @@ func buildXEPG(background bool) {
|
||||
|
||||
System.ScanInProgress = 1
|
||||
|
||||
var err error
|
||||
|
||||
Data.Cache.Images, err = imgcache.New(System.Folder.ImagesCache, fmt.Sprintf("%s://%s/images/", System.ServerProtocol.WEB, System.Domain), Settings.CacheImages)
|
||||
if err != nil {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
if Settings.EpgSource == "XEPG" {
|
||||
|
||||
switch background {
|
||||
@@ -56,10 +69,31 @@ func buildXEPG(background bool) {
|
||||
cleanupXEPG()
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
go cachingImages()
|
||||
queuePlexGuideRefresh("xepg rebuild")
|
||||
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 {
|
||||
|
||||
go func() {
|
||||
|
||||
System.ImageCachingInProgress = 1
|
||||
showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue)))
|
||||
|
||||
Data.Cache.Images.Image.Caching()
|
||||
Data.Cache.Images.Image.Remove()
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg image cache refresh")
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
System.ScanInProgress = 0
|
||||
|
||||
// Cache löschen
|
||||
@@ -82,7 +116,29 @@ func buildXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
go cachingImages()
|
||||
queuePlexGuideRefresh("xepg rebuild")
|
||||
|
||||
if Settings.CacheImages == true && System.ImageCachingInProgress == 0 {
|
||||
|
||||
go func() {
|
||||
|
||||
System.ImageCachingInProgress = 1
|
||||
showInfo(fmt.Sprintf("Image Caching:Images are cached (%d)", len(Data.Cache.Images.Queue)))
|
||||
|
||||
Data.Cache.Images.Image.Caching()
|
||||
Data.Cache.Images.Image.Remove()
|
||||
showInfo("Image Caching:Done")
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg image cache refresh")
|
||||
|
||||
System.ImageCachingInProgress = 0
|
||||
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
System.ScanInProgress = 0
|
||||
@@ -128,6 +184,7 @@ func updateXEPG(background bool) {
|
||||
|
||||
createXMLTVFile()
|
||||
createM3UFile()
|
||||
queuePlexGuideRefresh("xepg update")
|
||||
showInfo("XEPG:" + fmt.Sprintf("Ready to use"))
|
||||
|
||||
System.ScanInProgress = 0
|
||||
@@ -156,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
|
||||
@@ -199,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)
|
||||
@@ -220,7 +277,7 @@ func createXEPGMapping() {
|
||||
}
|
||||
|
||||
Data.XMLTV.Mapping = tmpMap
|
||||
tmpMap = make(map[string]interface{})
|
||||
tmpMap = make(map[string]any)
|
||||
|
||||
} else {
|
||||
|
||||
@@ -231,8 +288,8 @@ func createXEPGMapping() {
|
||||
}
|
||||
|
||||
// Auswahl für den Dummy erstellen
|
||||
var dummy = make(map[string]interface{})
|
||||
var times = []string{"30", "60", "90", "120"}
|
||||
var dummy = make(map[string]any)
|
||||
var times = []string{"30", "60", "90", "120", "180", "240", "360"}
|
||||
|
||||
for _, i := range times {
|
||||
|
||||
@@ -253,8 +310,9 @@ func createXEPGMapping() {
|
||||
// XEPG Datenbank erstellen / aktualisieren
|
||||
func createXEPGDatabase() (err error) {
|
||||
|
||||
var allChannelNumbers []float64
|
||||
Data.Cache.Streams.Active = []string{}
|
||||
var allChannelNumbers = make([]float64, 0, System.UnfilteredChannelLimit)
|
||||
Data.Cache.Streams.Active = make([]string, 0, System.UnfilteredChannelLimit)
|
||||
Data.XEPG.Channels = make(map[string]any, System.UnfilteredChannelLimit)
|
||||
|
||||
Data.XEPG.Channels, err = loadJSONFileToMap(System.File.XEPG)
|
||||
if err != nil {
|
||||
@@ -279,24 +337,33 @@ func createXEPGDatabase() (err error) {
|
||||
|
||||
var getFreeChannelNumber = func() (xChannelID string) {
|
||||
|
||||
sort.Float64s(allChannelNumbers)
|
||||
|
||||
var firstFreeNumber float64 = Settings.MappingFirstChannel
|
||||
|
||||
newNumber:
|
||||
for {
|
||||
|
||||
if indexOfFloat64(firstFreeNumber, allChannelNumbers) == -1 {
|
||||
xChannelID = fmt.Sprintf("%g", firstFreeNumber)
|
||||
allChannelNumbers = append(allChannelNumbers, firstFreeNumber)
|
||||
return
|
||||
}
|
||||
|
||||
if indexOfFloat64(firstFreeNumber, allChannelNumbers) == -1 {
|
||||
xChannelID = fmt.Sprintf("%g", firstFreeNumber)
|
||||
allChannelNumbers = append(allChannelNumbers, firstFreeNumber)
|
||||
} else {
|
||||
firstFreeNumber++
|
||||
goto newNumber
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var generateHashForChannel = func(m3uID string, groupTitle string, tvgID string, tvgName string, uuidKey string, uuidValue string) string {
|
||||
hash := md5.Sum([]byte(m3uID + groupTitle + tvgID + tvgName + uuidKey + uuidValue))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + "Update database")
|
||||
|
||||
// Kanal mit fehlenden Kanalnummern löschen
|
||||
// Kanal mit fehlenden Kanalnummern löschen. Delete channel with missing channel numbers
|
||||
for id, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
@@ -306,7 +373,6 @@ func createXEPGDatabase() (err error) {
|
||||
}
|
||||
|
||||
if len(xepgChannel.XChannelID) == 0 {
|
||||
fmt.Println(mapToJSON(xepgChannel))
|
||||
delete(Data.XEPG.Channels, id)
|
||||
}
|
||||
|
||||
@@ -316,11 +382,24 @@ func createXEPGDatabase() (err error) {
|
||||
|
||||
}
|
||||
|
||||
// Make a map of the db channels based on their previously downloaded attributes -- filename, group, title, etc
|
||||
var xepgChannelsValuesMap = make(map[string]XEPGChannelStruct, System.UnfilteredChannelLimit)
|
||||
for _, v := range Data.XEPG.Channels {
|
||||
var channel XEPGChannelStruct
|
||||
err = json.Unmarshal([]byte(mapToJSON(v)), &channel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
channelHash := generateHashForChannel(channel.FileM3UID, channel.GroupTitle, channel.TvgID, channel.TvgName, channel.UUIDKey, channel.UUIDValue)
|
||||
xepgChannelsValuesMap[channelHash] = channel
|
||||
}
|
||||
|
||||
for _, dsa := range Data.Streams.Active {
|
||||
|
||||
var channelExists = false // Entscheidet ob ein Kanal neu zu Datenbank hinzugefügt werden soll.
|
||||
var channelHasUUID = false // Überprüft, ob der Kanal (Stream) eindeutige ID's besitzt
|
||||
var currentXEPGID string // Aktuelle Datenbank ID (XEPG). Wird verwendet, um den Kanal in der Datenbank mit dem Stream der M3u zu aktualisieren
|
||||
var channelExists = false // Entscheidet ob ein Kanal neu zu Datenbank hinzugefügt werden soll. Decides whether a channel should be added to the database
|
||||
var channelHasUUID = false // Überprüft, ob der Kanal (Stream) eindeutige ID's besitzt. Checks whether the channel (stream) has unique IDs
|
||||
var currentXEPGID string // Aktuelle Datenbank ID (XEPG). Wird verwendet, um den Kanal in der Datenbank mit dem Stream der M3u zu aktualisieren. Current database ID (XEPG) Used to update the channel in the database with the stream of the M3u
|
||||
|
||||
var m3uChannel M3UChannelStructXEPG
|
||||
|
||||
err = json.Unmarshal([]byte(mapToJSON(dsa)), &m3uChannel)
|
||||
@@ -328,45 +407,55 @@ func createXEPGDatabase() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
Data.Cache.Streams.Active = append(Data.Cache.Streams.Active, m3uChannel.Name)
|
||||
Data.Cache.Streams.Active = append(Data.Cache.Streams.Active, m3uChannel.Name+m3uChannel.FileM3UID)
|
||||
|
||||
// XEPG Datenbank durchlaufen um nach dem Kanal zu suchen.
|
||||
for xepg, dxc := range Data.XEPG.Channels {
|
||||
|
||||
var xepgChannel XEPGChannelStruct
|
||||
err = json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
if err != nil {
|
||||
return
|
||||
// Try to find the channel based on matching all known values. If that fails, then move to full channel scan
|
||||
m3uChannelHash := generateHashForChannel(m3uChannel.FileM3UID, m3uChannel.GroupTitle, m3uChannel.TvgID, m3uChannel.TvgName, m3uChannel.UUIDKey, m3uChannel.UUIDValue)
|
||||
if val, ok := xepgChannelsValuesMap[m3uChannelHash]; ok {
|
||||
channelExists = true
|
||||
currentXEPGID = val.XEPG
|
||||
if len(m3uChannel.UUIDValue) > 0 {
|
||||
channelHasUUID = true
|
||||
}
|
||||
} else {
|
||||
|
||||
// Vergleichen des Streams anhand einer UUID in der M3U mit dem Kanal in der Databank
|
||||
if len(xepgChannel.UUIDValue) > 0 && len(m3uChannel.UUIDValue) > 0 {
|
||||
// XEPG Datenbank durchlaufen um nach dem Kanal zu suchen. Run through the XEPG database to search for the channel (full scan)
|
||||
for _, dxc := range xepgChannelsValuesMap {
|
||||
|
||||
if xepgChannel.UUIDValue == m3uChannel.UUIDValue && xepgChannel.UUIDKey == m3uChannel.UUIDKey {
|
||||
if m3uChannel.FileM3UID == dxc.FileM3UID {
|
||||
|
||||
channelExists = true
|
||||
channelHasUUID = true
|
||||
currentXEPGID = xepg
|
||||
break
|
||||
dxc.FileM3UID = m3uChannel.FileM3UID
|
||||
dxc.FileM3UName = m3uChannel.FileM3UName
|
||||
|
||||
// Vergleichen des Streams anhand einer UUID in der M3U mit dem Kanal in der Databank. Compare the stream using a UUID in the M3U with the channel in the database
|
||||
if len(dxc.UUIDValue) > 0 && len(m3uChannel.UUIDValue) > 0 {
|
||||
|
||||
if dxc.UUIDValue == m3uChannel.UUIDValue && dxc.UUIDKey == m3uChannel.UUIDKey {
|
||||
|
||||
channelExists = true
|
||||
channelHasUUID = true
|
||||
currentXEPGID = dxc.XEPG
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// Vergleichen des Streams mit dem Kanal in der Databank anhand des Kanalnamens. Compare the stream to the channel in the database using the channel name
|
||||
if dxc.Name == m3uChannel.Name {
|
||||
channelExists = true
|
||||
currentXEPGID = dxc.XEPG
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// Vergleichen des Streams mit dem Kanal in der Databank anhand des Kanalnamens
|
||||
//fmt.Println(xepgChannel.Name, xepgChannel.UUIDKey, xepgChannel.UUIDValue)
|
||||
if xepgChannel.Name == m3uChannel.Name {
|
||||
channelExists = true
|
||||
currentXEPGID = xepg
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//os.Exit(0)
|
||||
|
||||
switch channelExists {
|
||||
|
||||
case true:
|
||||
// Bereits vorhandener Kanal
|
||||
var xepgChannel XEPGChannelStruct
|
||||
@@ -429,7 +518,7 @@ func createXEPGDatabase() (err error) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + "Save DB file")
|
||||
err = saveMapToJSONFile(System.File.XEPG, Data.XEPG.Channels)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -438,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
|
||||
@@ -454,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
|
||||
|
||||
@@ -466,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
|
||||
}
|
||||
@@ -502,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 {
|
||||
@@ -516,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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -551,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
|
||||
@@ -562,6 +843,10 @@ func mapping() (err error) {
|
||||
// XMLTV Datei erstellen
|
||||
func createXMLTVFile() (err error) {
|
||||
|
||||
// Image Cache
|
||||
// 4edd81ab7c368208cc6448b615051b37.jpg
|
||||
var imgc = Data.Cache.Images
|
||||
|
||||
Data.Cache.ImagesFiles = []string{}
|
||||
Data.Cache.ImagesURLS = []string{}
|
||||
Data.Cache.ImagesCache = []string{}
|
||||
@@ -580,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
|
||||
}
|
||||
|
||||
@@ -609,7 +894,7 @@ func createXMLTVFile() (err error) {
|
||||
// Kanäle
|
||||
var channel Channel
|
||||
channel.ID = xepgChannel.XChannelID
|
||||
channel.Icon = Icon{Src: getCacheImageURL(xepgChannel.TvgLogo)}
|
||||
channel.Icon = Icon{Src: imgc.Image.GetURL(xepgChannel.TvgLogo)}
|
||||
channel.DisplayName = append(channel.DisplayName, DisplayName{Value: xepgChannel.XName})
|
||||
|
||||
xepgXML.Channel = append(xepgXML.Channel, &channel)
|
||||
@@ -635,9 +920,10 @@ func createXMLTVFile() (err error) {
|
||||
var xmlOutput = []byte(xml.Header + string(content))
|
||||
writeByteToFile(System.File.XML, xmlOutput)
|
||||
|
||||
xepgXML = XMLTV{}
|
||||
showInfo("XEPG:" + fmt.Sprintf("Compress XMLTV file (%s)", System.Compressed.GZxml))
|
||||
err = compressGZIP(&xmlOutput, System.Compressed.GZxml)
|
||||
|
||||
//saveMapToJSONFile(System.File.Images, Data.Cache.ImageCache)
|
||||
xepgXML = XMLTV{}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -647,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
|
||||
|
||||
@@ -656,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
|
||||
}
|
||||
|
||||
@@ -684,6 +981,15 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
// Category (Kategorie)
|
||||
getCategory(program, xmltvProgram, xepgChannel)
|
||||
|
||||
// Credits : (Credits)
|
||||
program.Credits = xmltvProgram.Credits
|
||||
|
||||
// Rating (Bewertung)
|
||||
program.Rating = xmltvProgram.Rating
|
||||
|
||||
// StarRating (Bewertung / Kritiken)
|
||||
program.StarRating = xmltvProgram.StarRating
|
||||
|
||||
// Country (Länder)
|
||||
program.Country = xmltvProgram.Country
|
||||
|
||||
@@ -711,25 +1017,33 @@ func getProgramData(xepgChannel XEPGChannelStruct) (xepgXML XMLTV, err error) {
|
||||
// Live
|
||||
program.Live = xmltvProgram.Live
|
||||
|
||||
// Premiere
|
||||
program.Premiere = xmltvProgram.Premiere
|
||||
|
||||
xepgXML.Program = append(xepgXML.Program, program)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(xepgXML.Program) == 0 && relaxedMissingEPGMode == true && xmltvFile != System.Folder.Data+"xTeVe Dummy" {
|
||||
fallbackToDummy()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Dummy Daten erstellen (createXMLTVFile)
|
||||
func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
|
||||
var imgc = Data.Cache.Images
|
||||
var currentTime = time.Now()
|
||||
var dateArray = strings.Fields(currentTime.String())
|
||||
var offset = " " + dateArray[2]
|
||||
var currentDay = currentTime.Format("20060102")
|
||||
var startTime, _ = time.Parse("20060102150405", currentDay+"000000")
|
||||
|
||||
showInfo("Create Dummy Guide:" + "Time offset" + offset + " - " + xepgChannel.XName)
|
||||
showDebug("Create Dummy Guide:"+"Time offset"+offset+" - "+xepgChannel.XName, 2)
|
||||
|
||||
var dl = strings.Split(xepgChannel.XMapping, "_")
|
||||
dummyLength, err := strconv.Atoi(dl[0])
|
||||
@@ -738,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))
|
||||
|
||||
@@ -753,10 +1067,15 @@ func createDummyProgram(xepgChannel XEPGChannelStruct) (dummyXMLTV XMLTV) {
|
||||
epg.Start = epgStartTime.Format("20060102150405") + offset
|
||||
epg.Stop = epgStopTime.Format("20060102150405") + offset
|
||||
epg.Title = append(epg.Title, &Title{Value: xepgChannel.XName + " (" + epgStartTime.Weekday().String()[0:2] + ". " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04") + ")", Lang: "en"})
|
||||
epg.Desc = append(epg.Desc, &Desc{Value: "xTeVe: (" + strconv.Itoa(dummyLength) + " Minutes) " + epgStartTime.Weekday().String() + " " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04"), Lang: "en"})
|
||||
|
||||
if len(xepgChannel.XDescription) == 0 {
|
||||
epg.Desc = append(epg.Desc, &Desc{Value: "xTeVe: (" + strconv.Itoa(dummyLength) + " Minutes) " + epgStartTime.Weekday().String() + " " + epgStartTime.Format("15:04") + " - " + epgStopTime.Format("15:04"), Lang: "en"})
|
||||
} else {
|
||||
epg.Desc = append(epg.Desc, &Desc{Value: xepgChannel.XDescription, Lang: "en"})
|
||||
}
|
||||
|
||||
if Settings.XepgReplaceMissingImages == true {
|
||||
poster.Src = getCacheImageURL(xepgChannel.TvgLogo)
|
||||
poster.Src = imgc.Image.GetURL(xepgChannel.TvgLogo)
|
||||
epg.Poster = append(epg.Poster, poster)
|
||||
}
|
||||
|
||||
@@ -803,8 +1122,10 @@ func getCategory(program *Program, xmltvProgram *Program, xepgChannel XEPGChanne
|
||||
// Programm Poster Cover aus der XMLTV Datei laden
|
||||
func getPoster(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelStruct) {
|
||||
|
||||
var imgc = Data.Cache.Images
|
||||
|
||||
for _, poster := range xmltvProgram.Poster {
|
||||
poster.Src = getCacheImageURL(poster.Src)
|
||||
poster.Src = imgc.Image.GetURL(poster.Src)
|
||||
program.Poster = append(program.Poster, poster)
|
||||
}
|
||||
|
||||
@@ -812,7 +1133,7 @@ func getPoster(program *Program, xmltvProgram *Program, xepgChannel XEPGChannelS
|
||||
|
||||
if len(xmltvProgram.Poster) == 0 {
|
||||
var poster Poster
|
||||
poster.Src = getCacheImageURL(xepgChannel.TvgLogo)
|
||||
poster.Src = imgc.Image.GetURL(poster.Src)
|
||||
program.Poster = append(program.Poster, poster)
|
||||
}
|
||||
|
||||
@@ -828,7 +1149,16 @@ func getEpisodeNum(program *Program, xmltvProgram *Program, xepgChannel XEPGChan
|
||||
if len(xepgChannel.XCategory) > 0 && xepgChannel.XCategory != "Movie" {
|
||||
|
||||
if len(xmltvProgram.EpisodeNum) == 0 {
|
||||
program.EpisodeNum = append(program.EpisodeNum, &EpisodeNum{Value: time.Now().Format("2006-01-02 15:04:05"), System: "original-air-date"})
|
||||
|
||||
var timeLayout = "20060102150405"
|
||||
|
||||
t, err := time.Parse(timeLayout, strings.Split(xmltvProgram.Start, " ")[0])
|
||||
if err == nil {
|
||||
program.EpisodeNum = append(program.EpisodeNum, &EpisodeNum{Value: t.Format("2006-01-02 15:04:05"), System: "original-air-date"})
|
||||
} else {
|
||||
ShowError(err, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -914,6 +1244,18 @@ func createM3UFile() {
|
||||
// XEPG Datenbank bereinigen
|
||||
func cleanupXEPG() {
|
||||
|
||||
//fmt.Println(Settings.Files.M3U)
|
||||
|
||||
var sourceIDs []string
|
||||
|
||||
for source := range Settings.Files.M3U {
|
||||
sourceIDs = append(sourceIDs, source)
|
||||
}
|
||||
|
||||
for source := range Settings.Files.HDHR {
|
||||
sourceIDs = append(sourceIDs, source)
|
||||
}
|
||||
|
||||
showInfo("XEPG:" + fmt.Sprintf("Cleanup database"))
|
||||
Data.XEPG.XEPGCount = 0
|
||||
|
||||
@@ -923,7 +1265,7 @@ func cleanupXEPG() {
|
||||
err := json.Unmarshal([]byte(mapToJSON(dxc)), &xepgChannel)
|
||||
if err == nil {
|
||||
|
||||
if indexOfString(xepgChannel.Name, Data.Cache.Streams.Active) == -1 {
|
||||
if indexOfString(xepgChannel.Name+xepgChannel.FileM3UID, Data.Cache.Streams.Active) == -1 {
|
||||
delete(Data.XEPG.Channels, id)
|
||||
} else {
|
||||
if xepgChannel.XActive == true {
|
||||
@@ -931,6 +1273,10 @@ func cleanupXEPG() {
|
||||
}
|
||||
}
|
||||
|
||||
if indexOfString(xepgChannel.FileM3UID, sourceIDs) == -1 {
|
||||
delete(Data.XEPG.Channels, id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
131
ts/base_ts.ts
131
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,9 +23,8 @@ 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.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent"))
|
||||
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"))
|
||||
|
||||
@@ -52,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) {
|
||||
@@ -145,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
|
||||
@@ -174,6 +265,7 @@ function selectAllChannels() {
|
||||
|
||||
}
|
||||
|
||||
LAST_BULK_CHECKBOX = null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -198,6 +290,7 @@ function bulkEdit() {
|
||||
(rows[i] as HTMLInputElement).checked = false
|
||||
}
|
||||
|
||||
LAST_BULK_CHECKBOX = null
|
||||
return
|
||||
}
|
||||
|
||||
@@ -310,7 +403,7 @@ function createSearchObj() {
|
||||
var data = SERVER["xepg"]["epgMapping"]
|
||||
var channels = getObjKeys(data)
|
||||
|
||||
var channelKeys:string[] = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title"]
|
||||
var channelKeys:string[] = ["x-active", "x-channelID", "x-name", "_file.m3u.name", "x-group-title", "x-xmltv-file"]
|
||||
|
||||
channels.forEach(id => {
|
||||
|
||||
@@ -331,7 +424,17 @@ function createSearchObj() {
|
||||
|
||||
} else {
|
||||
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " "
|
||||
if (key == "x-xmltv-file") {
|
||||
var xmltvFile = getValueFromProviderFile(data[id][key], "xmltv", "name")
|
||||
|
||||
if (xmltvFile != undefined) {
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + xmltvFile + " "
|
||||
}
|
||||
|
||||
} else {
|
||||
SEARCH_MAPPING[id] = SEARCH_MAPPING[id] + data[id][key] + " "
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -370,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;
|
||||
|
||||
}
|
||||
|
||||
340
ts/menu_ts.ts
340
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
|
||||
|
||||
}
|
||||
@@ -147,7 +148,7 @@ class Content {
|
||||
var cell:Cell = new Cell()
|
||||
cell.child = true
|
||||
cell.childType = "P"
|
||||
if (SERVER["settings"]["buffer"] == true) {
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
cell.value = data[key]["tuner"]
|
||||
} else {
|
||||
cell.value = "-"
|
||||
@@ -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
|
||||
@@ -1113,7 +1374,7 @@ function openPopUp(dataType, element) {
|
||||
content.appendRow("{{.playlist.fileM3U.title}}", input)
|
||||
|
||||
// Tuner
|
||||
if (SERVER["settings"]["buffer"] == true) {
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
var text:string[] = new Array()
|
||||
var values:string[] = new Array()
|
||||
|
||||
@@ -1192,7 +1453,7 @@ function openPopUp(dataType, element) {
|
||||
content.appendRow("{{.playlist.fileHDHR.title}}", input)
|
||||
|
||||
// Tuner
|
||||
if (SERVER["settings"]["buffer"] == true) {
|
||||
if (SERVER["settings"]["buffer"] != "-") {
|
||||
var text:string[] = new Array()
|
||||
var values:string[] = new Array()
|
||||
|
||||
@@ -1532,6 +1793,13 @@ function openPopUp(dataType, element) {
|
||||
|
||||
content.description(data["name"])
|
||||
|
||||
// Beschreibung
|
||||
var dbKey:string = "x-description"
|
||||
var input = content.createInput("text", dbKey, data[dbKey])
|
||||
input.setAttribute("placeholder", "{{.mapping.description.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
content.appendRow("{{.mapping.description.title}}", input)
|
||||
|
||||
// Aktualisierung des Kanalnamens
|
||||
if (data.hasOwnProperty("_uuid.key")) {
|
||||
if (data["_uuid.key"] != "") {
|
||||
@@ -1561,7 +1829,7 @@ function openPopUp(dataType, element) {
|
||||
// Erweitern der EPG Kategorie
|
||||
var dbKey:string = "x-category"
|
||||
var text:string[] = ["-", "Kids (Emby only)", "News", "Movie", "Series", "Sports"]
|
||||
var values:string[] = ["-", "Kids", "News", "Movie", "Series", "Sports"]
|
||||
var values:string[] = ["", "Kids", "News", "Movie", "Series", "Sports"]
|
||||
var select = content.createSelect(text, values, data[dbKey], dbKey)
|
||||
select.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
content.appendRow("{{.mapping.epgCategory.title}}", select)
|
||||
@@ -1572,6 +1840,10 @@ function openPopUp(dataType, element) {
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
content.appendRow("{{.mapping.m3uGroupTitle.title}}", input)
|
||||
|
||||
if (data["group-title"] != undefined) {
|
||||
content.description(data["group-title"])
|
||||
}
|
||||
|
||||
// XMLTV Datei
|
||||
var dbKey:string = "x-xmltv-file"
|
||||
var xmlFile = data[dbKey]
|
||||
|
||||
@@ -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}}" + ":"
|
||||
@@ -89,6 +118,62 @@ class SettingsCategory {
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "ffmpeg.path":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.ffmpegPath.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "ffmpeg.path", data)
|
||||
input.setAttribute("placeholder", "{{.settings.ffmpegPath.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "ffmpeg.options":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.ffmpegOptions.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "ffmpeg.options", data)
|
||||
input.setAttribute("placeholder", "{{.settings.ffmpegOptions.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "vlc.path":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.vlcPath.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "vlc.path", data)
|
||||
input.setAttribute("placeholder", "{{.settings.vlcPath.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "vlc.options":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.vlcOptions.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "vlc.options", data)
|
||||
input.setAttribute("placeholder", "{{.settings.vlcOptions.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
// Checkboxen
|
||||
case "authentication.web":
|
||||
var tdLeft = document.createElement("TD")
|
||||
@@ -216,20 +301,6 @@ class SettingsCategory {
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "buffer":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createCheckbox(settingsKey)
|
||||
input.checked = data
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "api":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.api.title}}" + ":"
|
||||
@@ -244,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()
|
||||
@@ -314,6 +415,37 @@ class SettingsCategory {
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
case "buffer":
|
||||
var tdLeft = document.createElement("TD")
|
||||
tdLeft.innerHTML = "{{.settings.streamBuffering.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var text:any[] = ["{{.settings.streamBuffering.info_false}}", "xTeVe: ({{.settings.streamBuffering.info_xteve}})", "FFmpeg: ({{.settings.streamBuffering.info_ffmpeg}})", "VLC: ({{.settings.streamBuffering.info_vlc}})"]
|
||||
var values:any[] = ["-", "xteve", "ffmpeg", "vlc"]
|
||||
|
||||
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 "udpxy":
|
||||
|
||||
var tdLeft = document.createElement("TD");
|
||||
tdLeft.innerHTML = "{{.settings.udpxy.title}}" + ":"
|
||||
|
||||
var tdRight = document.createElement("TD")
|
||||
var input = content.createInput("text", "udpxy", data)
|
||||
input.setAttribute("placeholder", "{{.settings.udpxy.placeholder}}")
|
||||
input.setAttribute("onchange", "javascript: this.className = 'changed'")
|
||||
tdRight.appendChild(input)
|
||||
|
||||
setting.appendChild(tdLeft)
|
||||
setting.appendChild(tdRight)
|
||||
break
|
||||
|
||||
}
|
||||
|
||||
return setting
|
||||
@@ -381,6 +513,30 @@ 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
|
||||
|
||||
case "ffmpeg.options":
|
||||
text = "{{.settings.ffmpegOptions.description}}"
|
||||
break
|
||||
|
||||
case "vlc.path":
|
||||
text = "{{.settings.vlcPath.description}}"
|
||||
break
|
||||
|
||||
case "vlc.options":
|
||||
text = "{{.settings.vlcOptions.description}}"
|
||||
break
|
||||
|
||||
case "epgSource":
|
||||
text = "{{.settings.epgSource.description}}"
|
||||
break
|
||||
@@ -397,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
|
||||
@@ -405,8 +565,16 @@ class SettingsCategory {
|
||||
text = "{{.settings.cacheImages.description}}"
|
||||
break
|
||||
|
||||
case "xepg.replace.missing.images":
|
||||
text = "{{.settings.replaceEmptyImages.description}}"
|
||||
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
|
||||
|
||||
default:
|
||||
|
||||
19
xteve.go
19
xteve.go
@@ -13,7 +13,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"./src"
|
||||
"xteve/src"
|
||||
)
|
||||
|
||||
// GitHubStruct : GitHub Account. Über diesen Account werden die Updates veröffentlicht
|
||||
@@ -35,21 +35,19 @@ var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-
|
||||
Update: Automatic updates from the GitHub repository [true|false]
|
||||
*/
|
||||
|
||||
// Name : Programname
|
||||
// Name : Programmname
|
||||
const Name = "xTeVe"
|
||||
|
||||
// Version : Version, die Build Nummer wird in der main func geparst.
|
||||
const Version = "2.0.1.0010"
|
||||
// Can be overwritten at build time: -ldflags "-X main.Version=..."
|
||||
var Version = "2.2.0.0200"
|
||||
|
||||
// DBVersion : Datanbank Version
|
||||
const DBVersion = "2.0.0"
|
||||
const DBVersion = "2.1.0"
|
||||
|
||||
// APIVersion : API Version
|
||||
const APIVersion = "1.1.0"
|
||||
|
||||
// Dev : Aktiviert den Entwicklungsmodus. Für den Webserver werden dann die lokalen Dateien verwendet.
|
||||
const Dev = false
|
||||
|
||||
var homeDirectory = fmt.Sprintf("%s%s.%s%s", src.GetUserHomeDirectory(), string(os.PathSeparator), strings.ToLower(Name), string(os.PathSeparator))
|
||||
var samplePath = fmt.Sprintf("%spath%sto%sxteve%s", string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator))
|
||||
var sampleRestore = fmt.Sprintf("%spath%sto%sfile%s", string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator), string(os.PathSeparator))
|
||||
@@ -63,6 +61,9 @@ var debug = flag.Int("debug", 0, ": Debug level [0 - 3] (default: 0)")
|
||||
var info = flag.Bool("info", false, ": Show system info")
|
||||
var h = flag.Bool("h", false, ": Show help")
|
||||
|
||||
// Aktiviert den Entwicklungsmodus. Für den Webserver werden dann die lokalen Dateien verwendet.
|
||||
var dev = flag.Bool("dev", false, ": Activates the developer mode, the source code must be available. The local files for the web interface are used.")
|
||||
|
||||
func main() {
|
||||
|
||||
// Build-Nummer von der Versionsnummer trennen
|
||||
@@ -73,7 +74,6 @@ func main() {
|
||||
system.Branch = GitHub.Branch
|
||||
system.Build = build[len(build)-1:][0]
|
||||
system.DBVersion = DBVersion
|
||||
system.Dev = Dev
|
||||
system.GitHub = GitHub
|
||||
system.Name = Name
|
||||
system.Version = strings.Join(build[0:len(build)-1], ".")
|
||||
@@ -122,6 +122,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
system.Dev = *dev
|
||||
|
||||
// Systeminformationen anzeigen
|
||||
if *info {
|
||||
|
||||
@@ -189,7 +191,6 @@ func main() {
|
||||
err = src.BinaryUpdate()
|
||||
if err != nil {
|
||||
src.ShowError(err, 0)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err = src.StartSystem(false)
|
||||
|
||||
Reference in New Issue
Block a user